-
Notifications
You must be signed in to change notification settings - Fork 28
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
Stripe Auto-refund cancellation within Grace Period #894
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅ ✅ All tests successful. No failed tests found. 📢 Thoughts on this report? Let us know! |
Codecov ReportAll modified and coverable lines are covered by tests ✅
✅ All tests successful. No failed tests found. Additional details and impacted files@@ Coverage Diff @@
## main #894 +/- ##
=======================================
Coverage 96.23% 96.23%
=======================================
Files 823 823
Lines 18972 19002 +30
=======================================
+ Hits 18257 18287 +30
Misses 715 715
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
5aad806
to
813eed3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
left some comments and slacked you with a few additional thoughts 👍
services/billing.py
Outdated
cancel_at_period_end=True, | ||
proration_behavior="none", | ||
# we give an auto-refund grace period of 24 hours for a monthly subscription or 72 hours for a yearly subscription | ||
# current_subscription_timestamp = subscription["current_period_start"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
did you mean to leave this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, for some descriptuon
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not the second line. oops
services/billing.py
Outdated
) | ||
differenceFromNow = datetime.now() - current_subscription_datetime |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
snake pls!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, haha habit
services/billing.py
Outdated
) | ||
within_refund_grace_period = ( | ||
subscription_plan_interval == "month" and differenceFromNow.days < 1 | ||
) or (subscription_plan_interval == "year" and differenceFromNow.days < 3) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
does .days
operate the way you expect? Did you check for weird rounding errors in how it defines a day?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.days is defined as full days so it will be an integer
services/billing.py
Outdated
) | ||
differenceFromNow = datetime.now() - current_subscription_datetime | ||
|
||
subscription_plan_interval = ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is equivalent to subscription_plan_interval = getattr(subscription.plan, "interval", None) I believe
services/billing.py
Outdated
): | ||
stripe.Refund.create(invoice["charge"]) | ||
created_refund = True | ||
if created_refund == True: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can get rid of this secondary if (and remove created_refund) and have this clause be in the top level statement with what you have in the else as an else for the conditional starting on 198, right?
stripe.Customer.modify( owner.stripe_customer_id, balance=0, )
0a029bb
to
8432340
Compare
❌ 3 Tests Failed:
View the top 3 failed tests by shortest run time
To view individual test run time comparison to the main branch, go to the Test Analytics Dashboard |
Test Failures Detected: Due to failing tests, we cannot provide coverage reports at this time. ❌ Failed Test Results:Completed 2647 tests with View the full list of failed testspytest
|
services/billing.py
Outdated
customer = stripe.Customer.retrieve(owner.stripe_customer_id) | ||
# we are giving customers 2 autorefund instances | ||
autorefunds_remaining = int( | ||
customer["metadata"].get("autorefunds_remaining", "2") | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want this logic to be inside the if within_refund_grace_period
block so it's only called when they are within the cancellation window
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense. Any ideas about how to prevent another level of if
statements or in your opinion, does that seem fine? Do we have a general rule of thumb for how many levels we think is acceptable?
cancel_at_period_end=True, | ||
proration_behavior="none", | ||
# we give an auto-refund grace period of 24 hours for a monthly subscription or 72 hours for a yearly subscription | ||
current_subscription_datetime = datetime.fromtimestamp( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to confirm, are the timezones for both datetime.now() and datetime.fromtimestamp() the same?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a great question. I just checked and both are in machine local so since we only care about the difference, we're good.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great! Thanks for checking
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm converting these to utc now actually because I realized depending on what the local time of the machine is, it could change the date. Like if february 29th was converted to march 1st due to timezone difference, subtracting 1 month would give february 1st instead of january 29th
services/billing.py
Outdated
if within_refund_grace_period and autorefunds_remaining > 0: | ||
stripe.Subscription.cancel(owner.stripe_subscription_id) | ||
|
||
invoices_list = stripe.Invoice.list( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it looks like you can add a created parameter to this call too and maybe shorten the amount of results to filter through as well. Not sure how much of an optimization... maybe to the point of not needing this stuff:
and invoice_created_datetime < current_subscription_datetime
and invoice_created_datetime >= start_of_last_period
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would allow you to this variable above and condense this for loop as well
start_of_last_period = (
current_subscription_datetime - relativedelta(months=1)
if subscription_plan_interval == "month"
else current_subscription_datetime - relativedelta(years=1)
)
services/billing.py
Outdated
balance=0, | ||
metadata={"autorefunds_remaining": str(autorefunds_remaining - 1)}, | ||
) | ||
log.info( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any other log info that might be useful here? Maybe customer id and sub id off the top of my head, we can add them with the extra=dict() param
8432340
to
3ecef75
Compare
services/billing.py
Outdated
# cancels a Stripe customer subscription immediately and attempts to refund their payments for the current period | ||
def cancel_and_refund( | ||
self, | ||
owner, | ||
current_subscription_datetime, | ||
subscription_plan_interval, | ||
autorefunds_remaining, | ||
): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
# cancels a Stripe customer subscription immediately and attempts to refund their payments for the current period | |
def cancel_and_refund( | |
self, | |
owner, | |
current_subscription_datetime, | |
subscription_plan_interval, | |
autorefunds_remaining, | |
): | |
def cancel_and_refund( | |
self, | |
owner, | |
current_subscription_datetime, | |
subscription_plan_interval, | |
autorefunds_remaining, | |
): | |
# cancels a Stripe customer subscription immediately and attempts to refund their payments for the current period |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
left 1 suggested change - the comment should be inside the func, other than that I think it's ready to go! 🚀
|
||
if created_refund: | ||
# update the customer's balance back to 0 in accordance to | ||
# https://support.stripe.com/questions/refunding-credit-balance-to-customer-after-subscription-downgrade-or-cancellation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ty for the documentation!!
autorefunds_remaining=autorefunds_remaining, | ||
), | ||
) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
great logs!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
very nice!
ab9bfe0
to
0d29795
Compare
e9eff0c
to
5c9eec8
Compare
Purpose/Motivation
What is the feature? Why is this being done?
Adding auto-refund logic for when a user cancels their subscription within 24 hours (monthly charge) or 72 hours (yearly charge) of the period starting and being charged.
Open to any and all feedback including styling and syntax recommendations as this is one of my first python/codecov-api PRs. Should I wrap Stripe API calls in a try/catch?
Links to relevant tickets
Closes codecov/engineering-team#2508
What does this PR do?
Our current logic is that when someone cancels, since they have already paid for their current period (either a month or a year), we just "modify" there subscription to cancel at period end. We will still keep this logic for when they cancel out of the grace period but within the grace period, we will cancel immediately and refund them their money.
Doing now:
Notable change
{"autorefunds_remaining: int"}
as opposed to making a new column in the DBOwners
table