Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit 27890c7

Browse files
trent-codecovajay-sentrycalvin-codecovnora-shap
authored
Trent codecov patch 1 (#942)
Co-authored-by: ajay-sentry <[email protected]> Co-authored-by: calvin-codecov <[email protected]> Co-authored-by: Nora Shapiro <[email protected]>
1 parent 90e7eb8 commit 27890c7

File tree

6 files changed

+489
-21
lines changed

6 files changed

+489
-21
lines changed

codecov/settings_base.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@
165165
"'sha256-eKdXhLyOdPl2/gp1Ob116rCU2Ox54rseyz1MwCmzb6w='",
166166
"'sha256-a1pELtDJXf8fPX1YL2JiBM91RQBeIAswunzgwMEsvwA='",
167167
"'sha256-cNIcuS0BVLuBVP5rpfeFE42xHz7r5hMyf9YdfknWuCg='",
168+
"'sha256-bmwAzHxhO1mBINfkKkKPopyKEv4ppCHx/z84wQJ9nOY='",
169+
"'sha256-jQoC6QpIonlMBPFbUGlJFRJFFWbbijMl7Z8XqWrb46o='",
168170
"https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/js/middleware.js",
169171
"https://cdn.jsdelivr.net/npm/graphql-playground-react/build/favicon.png",
170172
"https://cdn.jsdelivr.net/npm/graphql-playground-react/build/static/css/index.css",
@@ -350,7 +352,11 @@
350352
)
351353
CORS_ALLOWED_ORIGINS: list[str] = []
352354

353-
GRAPHQL_PLAYGROUND = True
355+
GRAPHQL_PLAYGROUND = get_settings_module() in [
356+
SettingsModule.DEV.value,
357+
SettingsModule.STAGING.value,
358+
SettingsModule.TESTING.value,
359+
]
354360

355361
UPLOAD_THROTTLING_ENABLED = get_config(
356362
"setup", "upload_throttling_enabled", default=True

requirements.in

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
aiodataloader
2-
ariadne
3-
ariadne_django
2+
ariadne==0.23
3+
ariadne_django==0.3.0
44
celery>=5.3.6
55
cerberus
66
ddtrace
7-
Django>=4.2.15
7+
Django>=4.2.16
88
django-cors-headers
99
django-csp
1010
django-dynamic-fixture
@@ -49,6 +49,7 @@ sentry-sdk>=2.13.0
4949
sentry-sdk[celery]
5050
setproctitle
5151
simplejson
52+
starlette==0.40.0
5253
stripe>=9.6.0
5354
urllib3>=1.26.19
5455
vcrpy

requirements.txt

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ anyio==3.6.1
1818
# starlette
1919
appdirs==1.4.4
2020
# via virtualenv
21-
ariadne==0.19.1
21+
ariadne==0.23.0
2222
# via
2323
# -r requirements.in
2424
# ariadne-django
25-
ariadne-django==0.2.0
25+
ariadne-django==0.3.0
2626
# via -r requirements.in
2727
asgiref==3.6.0
2828
# via django
@@ -109,7 +109,7 @@ deprecated==1.2.12
109109
# via opentelemetry-api
110110
distlib==0.3.1
111111
# via virtualenv
112-
django==4.2.15
112+
django==4.2.16
113113
# via
114114
# -r requirements.in
115115
# ariadne-django
@@ -177,7 +177,6 @@ freezegun==1.1.0
177177
# via -r requirements.in
178178
google-api-core[grpc]==2.11.1
179179
# via
180-
# google-api-core
181180
# google-cloud-core
182181
# google-cloud-pubsub
183182
# google-cloud-storage
@@ -409,9 +408,7 @@ requests==2.32.3
409408
# shared
410409
# stripe
411410
rfc3986[idna2008]==1.4.0
412-
# via
413-
# httpx
414-
# rfc3986
411+
# via httpx
415412
rsa==4.7.2
416413
# via google-auth
417414
s3transfer==0.5.0
@@ -450,8 +447,10 @@ sqlparse==0.5.0
450447
# via
451448
# ddtrace
452449
# django
453-
starlette==0.36.2
454-
# via ariadne
450+
starlette==0.40.0
451+
# via
452+
# -r requirements.in
453+
# ariadne
455454
stripe==9.6.0
456455
# via -r requirements.in
457456
text-unidecode==1.3

services/billing.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import logging
22
import re
33
from abc import ABC, abstractmethod
4+
from datetime import datetime, timezone
45

56
import stripe
7+
from dateutil.relativedelta import relativedelta
68
from django.conf import settings
79

810
from billing.constants import REMOVED_INVOICE_STATUSES
@@ -146,6 +148,87 @@ def list_filtered_invoices(self, owner: Owner, limit=10):
146148
)
147149
return list(invoices_filtered_by_status_and_total)
148150

151+
def cancel_and_refund(
152+
self,
153+
owner,
154+
current_subscription_datetime,
155+
subscription_plan_interval,
156+
autorefunds_remaining,
157+
):
158+
# cancels a Stripe customer subscription immediately and attempts to refund their payments for the current period
159+
stripe.Subscription.cancel(owner.stripe_subscription_id)
160+
161+
start_of_last_period = current_subscription_datetime - relativedelta(months=1)
162+
invoice_grace_period_start = current_subscription_datetime - relativedelta(
163+
days=1
164+
)
165+
166+
if subscription_plan_interval == "year":
167+
start_of_last_period = current_subscription_datetime - relativedelta(
168+
years=1
169+
)
170+
invoice_grace_period_start = current_subscription_datetime - relativedelta(
171+
days=3
172+
)
173+
174+
invoices_list = stripe.Invoice.list(
175+
subscription=owner.stripe_subscription_id,
176+
status="paid",
177+
created={
178+
"created.gte": int(start_of_last_period.timestamp()),
179+
"created.lt": int(current_subscription_datetime.timestamp()),
180+
},
181+
)
182+
183+
# we only want to refund the invoices PAID recently for the latest, current period. "invoices_list" gives us any invoice
184+
# created over the last month/year based on what period length they are on but the customer could have possibly
185+
# switched from monthly to yearly recently.
186+
recently_paid_invoices_list = [
187+
invoice
188+
for invoice in invoices_list["data"]
189+
if invoice["status_transitions"]["paid_at"] is not None
190+
and invoice["status_transitions"]["paid_at"]
191+
>= int(invoice_grace_period_start.timestamp())
192+
]
193+
194+
created_refund = False
195+
# there could be multiple invoices that need to be refunded such as if the user increased seats within the grace period
196+
for invoice in recently_paid_invoices_list:
197+
# refund if the invoice has a charge and it has been fully paid
198+
if invoice["charge"] is not None and invoice["amount_remaining"] == 0:
199+
stripe.Refund.create(invoice["charge"])
200+
created_refund = True
201+
202+
if created_refund:
203+
# update the customer's balance back to 0 in accordance to
204+
# https://support.stripe.com/questions/refunding-credit-balance-to-customer-after-subscription-downgrade-or-cancellation
205+
stripe.Customer.modify(
206+
owner.stripe_customer_id,
207+
balance=0,
208+
metadata={"autorefunds_remaining": str(autorefunds_remaining - 1)},
209+
)
210+
log.info(
211+
"Grace period cancelled a subscription and autorefunded associated invoices",
212+
extra=dict(
213+
owner_id=owner.ownerid,
214+
user_id=self.requesting_user.ownerid,
215+
subscription_id=owner.stripe_subscription_id,
216+
customer_id=owner.stripe_customer_id,
217+
autorefunds_remaining=autorefunds_remaining - 1,
218+
),
219+
)
220+
else:
221+
log.info(
222+
"Grace period cancelled a subscription but did not find any appropriate invoices to autorefund",
223+
extra=dict(
224+
owner_id=owner.ownerid,
225+
user_id=self.requesting_user.ownerid,
226+
subscription_id=owner.stripe_subscription_id,
227+
customer_id=owner.stripe_customer_id,
228+
autorefunds_remaining=autorefunds_remaining,
229+
),
230+
)
231+
149232
@_log_stripe_error
150233
def delete_subscription(self, owner: Owner):
151234
subscription = stripe.Subscription.retrieve(owner.stripe_subscription_id)
@@ -155,13 +238,52 @@ def delete_subscription(self, owner: Owner):
155238
f"Downgrade to basic plan from user plan for owner {owner.ownerid} by user #{self.requesting_user.ownerid}",
156239
extra=dict(ownerid=owner.ownerid),
157240
)
241+
158242
if subscription_schedule_id:
159243
log.info(
160244
f"Releasing subscription from schedule for owner {owner.ownerid} by user #{self.requesting_user.ownerid}",
161245
extra=dict(ownerid=owner.ownerid),
162246
)
163247
stripe.SubscriptionSchedule.release(subscription_schedule_id)
164248

249+
# we give an auto-refund grace period of 24 hours for a monthly subscription or 72 hours for a yearly subscription
250+
current_subscription_datetime = datetime.fromtimestamp(
251+
subscription["current_period_start"], tz=timezone.utc
252+
)
253+
difference_from_now = datetime.now(timezone.utc) - current_subscription_datetime
254+
255+
subscription_plan_interval = getattr(
256+
getattr(subscription, "plan", None), "interval", None
257+
)
258+
within_refund_grace_period = (
259+
subscription_plan_interval == "month" and difference_from_now.days < 1
260+
) or (subscription_plan_interval == "year" and difference_from_now.days < 3)
261+
262+
if within_refund_grace_period:
263+
customer = stripe.Customer.retrieve(owner.stripe_customer_id)
264+
# we are currently allowing customers 2 autorefund instances
265+
autorefunds_remaining = int(
266+
customer["metadata"].get("autorefunds_remaining", "2")
267+
)
268+
log.info(
269+
"Deleting subscription with attempted immediate cancellation with autorefund within grace period",
270+
extra=dict(
271+
owner_id=owner.ownerid,
272+
user_id=self.requesting_user.ownerid,
273+
subscription_id=owner.stripe_subscription_id,
274+
customer_id=owner.stripe_customer_id,
275+
autorefunds_remaining=autorefunds_remaining,
276+
),
277+
)
278+
if autorefunds_remaining > 0:
279+
return self.cancel_and_refund(
280+
owner,
281+
current_subscription_datetime,
282+
subscription_plan_interval,
283+
autorefunds_remaining,
284+
)
285+
286+
# schedule a cancellation at the end of the paid period with no refund
165287
stripe.Subscription.modify(
166288
owner.stripe_subscription_id,
167289
cancel_at_period_end=True,

services/tests/samples/stripe_invoice.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@
120120
"total_tax_amounts": [],
121121
"webhooks_delivered_at": 1489789437
122122
},
123-
{
123+
{
124124
"id": "in_19yTU92eZvKYlo2C7uDjvu69",
125125
"object": "invoice",
126126
"account_country": "US",

0 commit comments

Comments
 (0)