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

Commit 3e28326

Browse files
committed
Send email to admins on failed payment
1 parent 564cad4 commit 3e28326

File tree

3 files changed

+165
-4
lines changed

3 files changed

+165
-4
lines changed

billing/tests/test_views.py

Lines changed: 124 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import time
2-
from unittest.mock import patch
2+
from datetime import datetime
3+
from unittest.mock import call, patch
34

45
import stripe
56
from django.conf import settings
@@ -142,6 +143,11 @@ def test_invoice_payment_failed_sets_owner_delinquent_true(self):
142143
"object": {
143144
"customer": self.owner.stripe_customer_id,
144145
"subscription": self.owner.stripe_subscription_id,
146+
"default_payment_method": {
147+
"card": {"brand": "visa", "last4": 1234}
148+
},
149+
"total": 24000,
150+
"hosted_invoice_url": "https://stripe.com",
145151
}
146152
},
147153
}
@@ -165,6 +171,11 @@ def test_invoice_payment_failed_sets_multiple_owners_delinquent_true(self):
165171
"object": {
166172
"customer": self.owner.stripe_customer_id,
167173
"subscription": self.owner.stripe_subscription_id,
174+
"default_payment_method": {
175+
"card": {"brand": "visa", "last4": 1234}
176+
},
177+
"total": 24000,
178+
"hosted_invoice_url": "https://stripe.com",
168179
}
169180
},
170181
}
@@ -176,6 +187,118 @@ def test_invoice_payment_failed_sets_multiple_owners_delinquent_true(self):
176187
assert self.owner.delinquent is True
177188
assert self.other_owner.delinquent is True
178189

190+
@patch("services.task.TaskService.send_email")
191+
def test_invoice_payment_failed_sends_email_to_admins(self, mocked_send_email):
192+
non_admin = OwnerFactory(email="[email protected]")
193+
admin_1 = OwnerFactory(email="[email protected]")
194+
admin_2 = OwnerFactory(email="[email protected]")
195+
self.owner.admins = [admin_1.ownerid, admin_2.ownerid]
196+
self.owner.plan_activated_users = [non_admin.ownerid]
197+
self.owner.save()
198+
199+
response = self._send_event(
200+
payload={
201+
"type": "invoice.payment_failed",
202+
"data": {
203+
"object": {
204+
"customer": self.owner.stripe_customer_id,
205+
"subscription": self.owner.stripe_subscription_id,
206+
"default_payment_method": {
207+
"card": {"brand": "visa", "last4": 1234}
208+
},
209+
"total": 24000,
210+
"hosted_invoice_url": "https://stripe.com",
211+
}
212+
},
213+
}
214+
)
215+
216+
self.owner.refresh_from_db()
217+
assert response.status_code == status.HTTP_204_NO_CONTENT
218+
assert self.owner.delinquent is True
219+
220+
expected_calls = [
221+
call(
222+
to_addr=admin_1.email,
223+
subject="Your Codecov payment failed",
224+
template_name="failed-payment",
225+
name=admin_1.username,
226+
amount=240,
227+
card_type="visa",
228+
last_four=1234,
229+
cta_link="https://stripe.com",
230+
date=datetime.now().strftime("%B %-d, %Y"),
231+
),
232+
call(
233+
to_addr=admin_2.email,
234+
subject="Your Codecov payment failed",
235+
template_name="failed-payment",
236+
name=admin_2.username,
237+
amount=240,
238+
card_type="visa",
239+
last_four=1234,
240+
cta_link="https://stripe.com",
241+
date=datetime.now().strftime("%B %-d, %Y"),
242+
),
243+
]
244+
mocked_send_email.assert_has_calls(expected_calls)
245+
246+
@patch("services.task.TaskService.send_email")
247+
def test_invoice_payment_failed_sends_email_to_admins_no_card(
248+
self, mocked_send_email
249+
):
250+
non_admin = OwnerFactory(email="[email protected]")
251+
admin_1 = OwnerFactory(email="[email protected]")
252+
admin_2 = OwnerFactory(email="[email protected]")
253+
self.owner.admins = [admin_1.ownerid, admin_2.ownerid]
254+
self.owner.plan_activated_users = [non_admin.ownerid]
255+
self.owner.save()
256+
257+
response = self._send_event(
258+
payload={
259+
"type": "invoice.payment_failed",
260+
"data": {
261+
"object": {
262+
"customer": self.owner.stripe_customer_id,
263+
"subscription": self.owner.stripe_subscription_id,
264+
"default_payment_method": None,
265+
"total": 24000,
266+
"hosted_invoice_url": "https://stripe.com",
267+
}
268+
},
269+
}
270+
)
271+
272+
self.owner.refresh_from_db()
273+
assert response.status_code == status.HTTP_204_NO_CONTENT
274+
assert self.owner.delinquent is True
275+
276+
expected_calls = [
277+
call(
278+
to_addr=admin_1.email,
279+
subject="Your Codecov payment failed",
280+
template_name="failed-payment",
281+
name=admin_1.username,
282+
amount=240,
283+
card_type=None,
284+
last_four=None,
285+
cta_link="https://stripe.com",
286+
date=datetime.now().strftime("%B %-d, %Y"),
287+
),
288+
call(
289+
to_addr=admin_2.email,
290+
subject="Your Codecov payment failed",
291+
template_name="failed-payment",
292+
name=admin_2.username,
293+
amount=240,
294+
card_type=None,
295+
last_four=None,
296+
cta_link="https://stripe.com",
297+
date=datetime.now().strftime("%B %-d, %Y"),
298+
),
299+
]
300+
mocked_send_email.assert_has_calls(expected_calls)
301+
179302
def test_customer_subscription_deleted_sets_plan_to_free(self):
180303
self.owner.plan = "users-inappy"
181304
self.owner.plan_user_count = 20

billing/views.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from datetime import datetime
23
from typing import List
34

45
import stripe
@@ -12,6 +13,7 @@
1213

1314
from codecov_auth.models import Owner
1415
from plan.service import PlanService
16+
from services.task.task import TaskService
1517

1618
from .constants import StripeHTTPHeaders, StripeWebhookEvents
1719

@@ -63,6 +65,36 @@ def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
6365
owners.update(delinquent=True)
6466
self._log_updated(list(owners))
6567

68+
# Send failed payment email to all owner admins
69+
70+
admins: QuerySet[Owner] = Owner.objects.filter(
71+
pk__in={admin for owner in owners for admin in owner.admins}
72+
)
73+
74+
task_service = TaskService()
75+
card = (
76+
invoice.default_payment_method.card
77+
if invoice.default_payment_method
78+
else None
79+
)
80+
template_vars = {
81+
"amount": invoice.total / 100,
82+
"card_type": card.brand if card else None,
83+
"last_four": card.last4 if card else None,
84+
"cta_link": invoice.hosted_invoice_url,
85+
"date": datetime.now().strftime("%B %-d, %Y"),
86+
}
87+
88+
for admin in admins:
89+
if admin.email is not None:
90+
task_service.send_email(
91+
to_addr=admin.email,
92+
subject="Your Codecov payment failed",
93+
template_name="failed-payment",
94+
name=admin.username,
95+
**template_vars,
96+
)
97+
6698
def customer_subscription_deleted(self, subscription: stripe.Subscription) -> None:
6799
log.info(
68100
"Customer Subscription Deleted - Setting free plan and deactivating repos for stripe customer",

services/task/task.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,15 +391,21 @@ def preprocess_upload(self, repoid, commitid, report_code):
391391
).apply_async()
392392

393393
def send_email(
394-
self, ownerid, template_name: str, from_addr: str, subject: str, **kwargs
394+
self,
395+
to_addr: str,
396+
subject: str,
397+
template_name: str,
398+
from_addr: str | None = None,
399+
**kwargs,
395400
):
401+
# Templates can be found in worker/templates
396402
self._create_signature(
397403
"app.tasks.send_email.SendEmail",
398404
kwargs=dict(
399-
ownerid=ownerid,
405+
to_addr=to_addr,
406+
subject=subject,
400407
template_name=template_name,
401408
from_addr=from_addr,
402-
subject=subject,
403409
**kwargs,
404410
),
405411
).apply_async()

0 commit comments

Comments
 (0)