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

Commit 24f0a7a

Browse files
feat: Add stripe metadata limit for autorefunds allowed
1 parent 008935e commit 24f0a7a

File tree

2 files changed

+47
-7
lines changed

2 files changed

+47
-7
lines changed

services/billing.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,6 @@ 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
)
@@ -178,7 +177,12 @@ def delete_subscription(self, owner: Owner):
178177
within_refund_grace_period = (
179178
subscription_plan_interval == "month" and differenceFromNow.days < 1
180179
) or (subscription_plan_interval == "year" and differenceFromNow.days < 3)
181-
if within_refund_grace_period:
180+
181+
customer = stripe.Customer.retrieve(owner.stripe_customer_id)
182+
# we are giving customers 2 autorefund instances
183+
autorefunds_remaining = int(customer["metadata"].get("autorefunds_remaining", '2'))
184+
185+
if within_refund_grace_period and autorefunds_remaining > 0:
182186
stripe.Subscription.cancel(owner.stripe_subscription_id)
183187

184188
invoices_list = stripe.Invoice.list(
@@ -193,7 +197,8 @@ def delete_subscription(self, owner: Owner):
193197
else current_subscription_datetime - relativedelta(years=1)
194198
)
195199

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
200+
# refund if all of the following are true: the invoice has a charge, it has been fully paid,
201+
# 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
197202
invoice_created_datetime = datetime.fromtimestamp(invoice["created"])
198203
if (
199204
invoice["charge"] is not None
@@ -207,6 +212,10 @@ def delete_subscription(self, owner: Owner):
207212
stripe.Customer.modify(
208213
owner.stripe_customer_id,
209214
balance=0,
215+
metadata={"autorefunds_remaining": str(autorefunds_remaining - 1)},
216+
)
217+
log.info(
218+
f"Autorefunded owner id #{owner.ownerid} for stripe id {owner.stripe_customer_id}. They have {str(autorefunds_remaining - 1)} remaining."
210219
)
211220
else:
212221
# 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: 35 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,17 @@ 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 = { "id": "cus_HF6p8Zx7JdRS7A", "metadata": {}}
417441
self.stripe.delete_subscription(owner)
418442
schedule_release_mock.assert_called_once_with(stripe_schedule_id)
443+
retrieve_customer_mock.assert_called_once_with(owner.stripe_customer_id)
419444
cancel_sub_mock.assert_called_once_with(stripe_subscription_id)
420445
list_invoice_mock.assert_called_once_with(
421446
subscription=stripe_subscription_id, status="paid"
422447
)
423448
self.assertEqual(create_refund_mock.call_count, 2)
424449
modify_customer_mock.assert_called_once_with(
425-
owner.stripe_customer_id, balance=0
450+
owner.stripe_customer_id, balance=0, metadata={"autorefunds_remaining":"1"}
426451
)
427452
modify_sub_mock.assert_not_called()
428453

@@ -433,6 +458,7 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri
433458
assert owner.plan_user_count == 9
434459

435460
@freeze_time("2017-03-19T00:00:00")
461+
@patch("services.billing.stripe.Customer.retrieve")
436462
@patch("services.billing.stripe.Subscription.modify")
437463
@patch("services.billing.stripe.Customer.modify")
438464
@patch("services.billing.stripe.Refund.create")
@@ -449,18 +475,21 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri
449475
create_refund_mock,
450476
modify_customer_mock,
451477
modify_sub_mock,
478+
retrieve_customer_mock,
452479
):
453480
with open("./services/tests/samples/stripe_invoice.json") as f:
454481
stripe_invoice_response = json.load(f)
455482
list_invoice_mock.return_value = stripe_invoice_response
456483
plan = PlanName.CODECOV_PRO_YEARLY.value
457484
stripe_subscription_id = "sub_1K77Y5GlVGuVgOrkJrLjRnne"
458485
stripe_schedule_id = "sub_sched_sch1K77Y5GlVGuVgOrkJrLjRnne"
486+
customer_id = "cus_HF6p8Zx7JdRS7A"
459487
owner = OwnerFactory(
460488
stripe_subscription_id=stripe_subscription_id,
461489
plan=plan,
462490
plan_activated_users=[4, 6, 3],
463491
plan_user_count=9,
492+
stripe_customer_id=customer_id
464493
)
465494
subscription_params = {
466495
"schedule_id": stripe_schedule_id,
@@ -478,15 +507,17 @@ def test_delete_subscription_with_schedule_releases_schedule_and_cancels_subscri
478507
}
479508

480509
retrieve_subscription_mock.return_value = MockSubscription(subscription_params)
510+
retrieve_customer_mock.return_value = { "id": "cus_HF6p8Zx7JdRS7A", "metadata": { "autorefunds_remaining": "1" }}
481511
self.stripe.delete_subscription(owner)
482512
schedule_release_mock.assert_called_once_with(stripe_schedule_id)
513+
retrieve_customer_mock.assert_called_once_with(owner.stripe_customer_id)
483514
cancel_sub_mock.assert_called_once_with(stripe_subscription_id)
484515
list_invoice_mock.assert_called_once_with(
485516
subscription=stripe_subscription_id, status="paid"
486517
)
487518
self.assertEqual(create_refund_mock.call_count, 2)
488519
modify_customer_mock.assert_called_once_with(
489-
owner.stripe_customer_id, balance=0
520+
owner.stripe_customer_id, balance=0, metadata={"autorefunds_remaining":"0"}
490521
)
491522
modify_sub_mock.assert_not_called()
492523

0 commit comments

Comments
 (0)