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

Commit 2570160

Browse files
feat: Add stripe metadata limit for autorefunds allowed
1 parent f691ddc commit 2570160

File tree

2 files changed

+60
-12
lines changed

2 files changed

+60
-12
lines changed

services/billing.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -166,19 +166,25 @@ def delete_subscription(self, owner: Owner):
166166
stripe.SubscriptionSchedule.release(subscription_schedule_id)
167167

168168
# we give an auto-refund grace period of 24 hours for a monthly subscription or 72 hours for a yearly subscription
169-
# current_subscription_timestamp = subscription["current_period_start"]
170169
current_subscription_datetime = datetime.fromtimestamp(
171170
subscription["current_period_start"]
172171
)
173-
differenceFromNow = datetime.now() - current_subscription_datetime
172+
difference_from_now = datetime.now() - current_subscription_datetime
174173

175-
subscription_plan_interval = (
176-
subscription.plan.interval if subscription.plan is not None else None
174+
subscription_plan_interval = getattr(
175+
getattr(subscription, "plan", None), "interval", None
177176
)
178177
within_refund_grace_period = (
179-
subscription_plan_interval == "month" and differenceFromNow.days < 1
180-
) or (subscription_plan_interval == "year" and differenceFromNow.days < 3)
181-
if within_refund_grace_period:
178+
subscription_plan_interval == "month" and difference_from_now.days < 1
179+
) or (subscription_plan_interval == "year" and difference_from_now.days < 3)
180+
181+
customer = stripe.Customer.retrieve(owner.stripe_customer_id)
182+
# we are giving customers 2 autorefund instances
183+
autorefunds_remaining = int(
184+
customer["metadata"].get("autorefunds_remaining", "2")
185+
)
186+
187+
if within_refund_grace_period and autorefunds_remaining > 0:
182188
stripe.Subscription.cancel(owner.stripe_subscription_id)
183189

184190
invoices_list = stripe.Invoice.list(
@@ -193,7 +199,8 @@ def delete_subscription(self, owner: Owner):
193199
else current_subscription_datetime - relativedelta(years=1)
194200
)
195201

196-
# refund if the invoice has a charge, it has been fully paid, the creation time was before the start of the current subscription's start and the creation time was after the start of the last period
202+
# refund if all of the following are true: the invoice has a charge, it has been fully paid,
203+
# the creation time was before the start of the current subscription's start, and the creation time was after the start of the last period
197204
invoice_created_datetime = datetime.fromtimestamp(invoice["created"])
198205
if (
199206
invoice["charge"] is not None
@@ -207,6 +214,10 @@ def delete_subscription(self, owner: Owner):
207214
stripe.Customer.modify(
208215
owner.stripe_customer_id,
209216
balance=0,
217+
metadata={"autorefunds_remaining": str(autorefunds_remaining - 1)},
218+
)
219+
log.info(
220+
f"Autorefunded owner id #{owner.ownerid} for stripe id #{owner.stripe_customer_id}. They have {str(autorefunds_remaining - 1)} remaining."
210221
)
211222
else:
212223
# outside of the grace period, we schedule a cancellation at the end of the period with no refund

services/tests/test_billing.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
import requests
55
from django.conf import settings
66
from django.test import TestCase
7-
from shared.django_apps.core.tests.factories import OwnerFactory
87
from freezegun import freeze_time
8+
from shared.django_apps.core.tests.factories import OwnerFactory
99
from stripe import InvalidRequestError
1010

1111
from codecov_auth.models import Service
@@ -287,19 +287,22 @@ def test_list_filtered_invoices_returns_emptylist_if_stripe_customer_id_is_None(
287287
invoice_list_mock.assert_not_called()
288288
assert invoices == []
289289

290+
@patch("services.billing.stripe.Customer.retrieve")
290291
@patch("services.billing.stripe.Subscription.retrieve")
291292
@patch("services.billing.stripe.Subscription.modify")
292293
def test_delete_subscription_without_schedule_modifies_subscription_to_delete_at_end_of_billing_cycle_if_valid_plan(
293-
self, modify_mock, retrieve_subscription_mock
294+
self, modify_mock, retrieve_subscription_mock, retrieve_customer_mock
294295
):
295296
plan = PlanName.CODECOV_PRO_YEARLY.value
296297
stripe_subscription_id = "sub_1K77Y5GlVGuVgOrkJrLjRnne"
297298
stripe_schedule_id = None
299+
customer_id = "cus_HF6p8Zx7JdRS7A"
298300
owner = OwnerFactory(
299301
stripe_subscription_id=stripe_subscription_id,
300302
plan=plan,
301303
plan_activated_users=[4, 6, 3],
302304
plan_user_count=9,
305+
stripe_customer_id=customer_id,
303306
)
304307
subscription_params = {
305308
"schedule_id": stripe_schedule_id,
@@ -311,6 +314,12 @@ def test_delete_subscription_without_schedule_modifies_subscription_to_delete_at
311314
}
312315

313316
retrieve_subscription_mock.return_value = MockSubscription(subscription_params)
317+
retrieve_customer_mock.return_value = {
318+
"id": "cus_123456789",
319+
"email": "[email protected]",
320+
"name": "Test User",
321+
"metadata": {},
322+
}
314323
self.stripe.delete_subscription(owner)
315324
modify_mock.assert_called_once_with(
316325
stripe_subscription_id,
@@ -324,6 +333,7 @@ def test_delete_subscription_without_schedule_modifies_subscription_to_delete_at
324333
assert owner.plan_user_count == 9
325334

326335
@freeze_time("2017-03-22T00:00:00")
336+
@patch("services.billing.stripe.Customer.retrieve")
327337
@patch("services.billing.stripe.Refund.create")
328338
@patch("services.billing.stripe.Subscription.modify")
329339
@patch("services.billing.stripe.Subscription.retrieve")
@@ -334,15 +344,18 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri
334344
retrieve_subscription_mock,
335345
modify_mock,
336346
create_refund_mock,
347+
retrieve_customer_mock,
337348
):
338349
plan = PlanName.CODECOV_PRO_YEARLY.value
339350
stripe_subscription_id = "sub_1K77Y5GlVGuVgOrkJrLjRnne"
340351
stripe_schedule_id = "sub_sched_sch1K77Y5GlVGuVgOrkJrLjRnne"
352+
# customer_id = "cus_HF6p8Zx7JdRS7A"
341353
owner = OwnerFactory(
342354
stripe_subscription_id=stripe_subscription_id,
343355
plan=plan,
344356
plan_activated_users=[4, 6, 3],
345357
plan_user_count=9,
358+
# stripe_customer_id=customer_id
346359
)
347360
subscription_params = {
348361
"schedule_id": stripe_schedule_id,
@@ -354,6 +367,12 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri
354367
}
355368

356369
retrieve_subscription_mock.return_value = MockSubscription(subscription_params)
370+
retrieve_customer_mock.return_value = {
371+
"id": "cus_123456789",
372+
"email": "[email protected]",
373+
"name": "Test User",
374+
"metadata": {},
375+
}
357376
self.stripe.delete_subscription(owner)
358377
schedule_release_mock.assert_called_once_with(stripe_schedule_id)
359378
modify_mock.assert_called_once_with(
@@ -376,8 +395,10 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri
376395
@patch("services.billing.stripe.Subscription.cancel")
377396
@patch("services.billing.stripe.Subscription.retrieve")
378397
@patch("services.billing.stripe.SubscriptionSchedule.release")
398+
@patch("services.billing.stripe.Customer.retrieve")
379399
def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscription_with_grace_month_refund_if_valid_plan(
380400
self,
401+
retrieve_customer_mock,
381402
schedule_release_mock,
382403
retrieve_subscription_mock,
383404
cancel_sub_mock,
@@ -392,11 +413,13 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri
392413
plan = PlanName.CODECOV_PRO_YEARLY.value
393414
stripe_subscription_id = "sub_1K77Y5GlVGuVgOrkJrLjRnne"
394415
stripe_schedule_id = "sub_sched_sch1K77Y5GlVGuVgOrkJrLjRnne"
416+
customer_id = "cus_HF6p8Zx7JdRS7A"
395417
owner = OwnerFactory(
396418
stripe_subscription_id=stripe_subscription_id,
397419
plan=plan,
398420
plan_activated_users=[4, 6, 3],
399421
plan_user_count=9,
422+
stripe_customer_id=customer_id,
400423
)
401424
subscription_params = {
402425
"schedule_id": stripe_schedule_id,
@@ -414,15 +437,20 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri
414437
}
415438

416439
retrieve_subscription_mock.return_value = MockSubscription(subscription_params)
440+
retrieve_customer_mock.return_value = {
441+
"id": "cus_HF6p8Zx7JdRS7A",
442+
"metadata": {},
443+
}
417444
self.stripe.delete_subscription(owner)
418445
schedule_release_mock.assert_called_once_with(stripe_schedule_id)
446+
retrieve_customer_mock.assert_called_once_with(owner.stripe_customer_id)
419447
cancel_sub_mock.assert_called_once_with(stripe_subscription_id)
420448
list_invoice_mock.assert_called_once_with(
421449
subscription=stripe_subscription_id, status="paid"
422450
)
423451
self.assertEqual(create_refund_mock.call_count, 2)
424452
modify_customer_mock.assert_called_once_with(
425-
owner.stripe_customer_id, balance=0
453+
owner.stripe_customer_id, balance=0, metadata={"autorefunds_remaining": "1"}
426454
)
427455
modify_sub_mock.assert_not_called()
428456

@@ -433,6 +461,7 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri
433461
assert owner.plan_user_count == 9
434462

435463
@freeze_time("2017-03-19T00:00:00")
464+
@patch("services.billing.stripe.Customer.retrieve")
436465
@patch("services.billing.stripe.Subscription.modify")
437466
@patch("services.billing.stripe.Customer.modify")
438467
@patch("services.billing.stripe.Refund.create")
@@ -449,18 +478,21 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri
449478
create_refund_mock,
450479
modify_customer_mock,
451480
modify_sub_mock,
481+
retrieve_customer_mock,
452482
):
453483
with open("./services/tests/samples/stripe_invoice.json") as f:
454484
stripe_invoice_response = json.load(f)
455485
list_invoice_mock.return_value = stripe_invoice_response
456486
plan = PlanName.CODECOV_PRO_YEARLY.value
457487
stripe_subscription_id = "sub_1K77Y5GlVGuVgOrkJrLjRnne"
458488
stripe_schedule_id = "sub_sched_sch1K77Y5GlVGuVgOrkJrLjRnne"
489+
customer_id = "cus_HF6p8Zx7JdRS7A"
459490
owner = OwnerFactory(
460491
stripe_subscription_id=stripe_subscription_id,
461492
plan=plan,
462493
plan_activated_users=[4, 6, 3],
463494
plan_user_count=9,
495+
stripe_customer_id=customer_id,
464496
)
465497
subscription_params = {
466498
"schedule_id": stripe_schedule_id,
@@ -478,15 +510,20 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri
478510
}
479511

480512
retrieve_subscription_mock.return_value = MockSubscription(subscription_params)
513+
retrieve_customer_mock.return_value = {
514+
"id": "cus_HF6p8Zx7JdRS7A",
515+
"metadata": {"autorefunds_remaining": "1"},
516+
}
481517
self.stripe.delete_subscription(owner)
482518
schedule_release_mock.assert_called_once_with(stripe_schedule_id)
519+
retrieve_customer_mock.assert_called_once_with(owner.stripe_customer_id)
483520
cancel_sub_mock.assert_called_once_with(stripe_subscription_id)
484521
list_invoice_mock.assert_called_once_with(
485522
subscription=stripe_subscription_id, status="paid"
486523
)
487524
self.assertEqual(create_refund_mock.call_count, 2)
488525
modify_customer_mock.assert_called_once_with(
489-
owner.stripe_customer_id, balance=0
526+
owner.stripe_customer_id, balance=0, metadata={"autorefunds_remaining": "0"}
490527
)
491528
modify_sub_mock.assert_not_called()
492529

0 commit comments

Comments
 (0)