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

Commit c23e09d

Browse files
committed
Merge branch 'main' into plan-repre
2 parents ff6c17f + 9958e78 commit c23e09d

File tree

11 files changed

+303
-53
lines changed

11 files changed

+303
-53
lines changed

billing/tests/test_views.py

Lines changed: 160 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,35 @@ def __getitem__(self, key):
3939
return getattr(self, key)
4040

4141

42+
class MockCard(object):
43+
def __init__(self):
44+
self.brand = "visa"
45+
self.last4 = "1234"
46+
47+
def __getitem__(self, key):
48+
return getattr(self, key)
49+
50+
51+
class MockPaymentMethod(object):
52+
def __init__(self, noCard=False):
53+
if noCard:
54+
self.card = None
55+
return
56+
57+
self.card = MockCard()
58+
59+
def __getitem__(self, key):
60+
return getattr(self, key)
61+
62+
63+
class MockPaymentIntent(object):
64+
def __init__(self, noCard=False):
65+
self.payment_method = MockPaymentMethod(noCard)
66+
67+
def __getitem__(self, key):
68+
return getattr(self, key)
69+
70+
4271
class StripeWebhookHandlerTests(APITestCase):
4372
def setUp(self):
4473
self.owner = OwnerFactory(
@@ -97,6 +126,8 @@ def test_invoice_payment_succeeded_sets_owner_delinquent_false(self):
97126
"object": {
98127
"customer": self.owner.stripe_customer_id,
99128
"subscription": self.owner.stripe_subscription_id,
129+
"total": 24000,
130+
"hosted_invoice_url": "https://stripe.com",
100131
}
101132
},
102133
}
@@ -120,6 +151,72 @@ def test_invoice_payment_succeeded_sets_multiple_owners_delinquent_false(self):
120151
"object": {
121152
"customer": self.owner.stripe_customer_id,
122153
"subscription": self.owner.stripe_subscription_id,
154+
"total": 24000,
155+
"hosted_invoice_url": "https://stripe.com",
156+
}
157+
},
158+
}
159+
)
160+
161+
self.owner.refresh_from_db()
162+
self.other_owner.refresh_from_db()
163+
assert response.status_code == status.HTTP_204_NO_CONTENT
164+
assert self.owner.delinquent is False
165+
assert self.other_owner.delinquent is False
166+
167+
@patch("services.task.TaskService.send_email")
168+
def test_invoice_payment_succeeded_emails_only_emails_delinquents(
169+
self,
170+
mocked_send_email,
171+
):
172+
self.add_second_owner()
173+
self.owner.delinquent = False
174+
self.owner.save()
175+
176+
response = self._send_event(
177+
payload={
178+
"type": "invoice.payment_succeeded",
179+
"data": {
180+
"object": {
181+
"customer": self.owner.stripe_customer_id,
182+
"subscription": self.owner.stripe_subscription_id,
183+
"total": 24000,
184+
"hosted_invoice_url": "https://stripe.com",
185+
}
186+
},
187+
}
188+
)
189+
190+
self.owner.refresh_from_db()
191+
self.other_owner.refresh_from_db()
192+
assert response.status_code == status.HTTP_204_NO_CONTENT
193+
assert self.owner.delinquent is False
194+
195+
mocked_send_email.assert_not_called()
196+
197+
@patch("services.task.TaskService.send_email")
198+
def test_invoice_payment_succeeded_emails_delinquents(self, mocked_send_email):
199+
non_admin = OwnerFactory(email="[email protected]")
200+
admin_1 = OwnerFactory(email="[email protected]")
201+
admin_2 = OwnerFactory(email="[email protected]")
202+
self.owner.admins = [admin_1.ownerid, admin_2.ownerid]
203+
self.owner.plan_activated_users = [non_admin.ownerid]
204+
self.owner.email = "[email protected]"
205+
self.owner.delinquent = True
206+
self.owner.save()
207+
self.add_second_owner()
208+
self.other_owner.delinquent = False
209+
self.other_owner.save()
210+
211+
response = self._send_event(
212+
payload={
213+
"type": "invoice.payment_succeeded",
214+
"data": {
215+
"object": {
216+
"customer": self.owner.stripe_customer_id,
217+
"subscription": self.owner.stripe_subscription_id,
218+
"total": 24000,
219+
"hosted_invoice_url": "https://stripe.com",
123220
}
124221
},
125222
}
@@ -131,22 +228,54 @@ def test_invoice_payment_succeeded_sets_multiple_owners_delinquent_false(self):
131228
assert self.owner.delinquent is False
132229
assert self.other_owner.delinquent is False
133230

134-
def test_invoice_payment_failed_sets_owner_delinquent_true(self):
231+
expected_calls = [
232+
call(
233+
to_addr=self.owner.email,
234+
subject="You're all set",
235+
template_name="success-after-failed-payment",
236+
amount=240,
237+
cta_link="https://stripe.com",
238+
date=datetime.now().strftime("%B %-d, %Y"),
239+
),
240+
call(
241+
to_addr=admin_1.email,
242+
subject="You're all set",
243+
template_name="success-after-failed-payment",
244+
amount=240,
245+
cta_link="https://stripe.com",
246+
date=datetime.now().strftime("%B %-d, %Y"),
247+
),
248+
call(
249+
to_addr=admin_2.email,
250+
subject="You're all set",
251+
template_name="success-after-failed-payment",
252+
amount=240,
253+
cta_link="https://stripe.com",
254+
date=datetime.now().strftime("%B %-d, %Y"),
255+
),
256+
]
257+
258+
mocked_send_email.assert_has_calls(expected_calls)
259+
260+
@patch("services.billing.stripe.PaymentIntent.retrieve")
261+
def test_invoice_payment_failed_sets_owner_delinquent_true(
262+
self, retrieve_paymentintent_mock
263+
):
135264
self.owner.delinquent = False
136265
self.owner.save()
137266

267+
retrieve_paymentintent_mock.return_value = MockPaymentIntent()
268+
138269
response = self._send_event(
139270
payload={
140271
"type": "invoice.payment_failed",
141272
"data": {
142273
"object": {
143274
"customer": self.owner.stripe_customer_id,
144275
"subscription": self.owner.stripe_subscription_id,
145-
"default_payment_method": {
146-
"card": {"brand": "visa", "last4": 1234}
147-
},
148276
"total": 24000,
149277
"hosted_invoice_url": "https://stripe.com",
278+
"payment_intent": "payment_intent_asdf",
150279
}
151280
},
152281
}
@@ -156,25 +285,28 @@ def test_invoice_payment_failed_sets_owner_delinquent_true(self):
156285
assert response.status_code == status.HTTP_204_NO_CONTENT
157286
assert self.owner.delinquent is True
158287

159-
def test_invoice_payment_failed_sets_multiple_owners_delinquent_true(self):
288+
@patch("services.billing.stripe.PaymentIntent.retrieve")
289+
def test_invoice_payment_failed_sets_multiple_owners_delinquent_true(
290+
self, retrieve_paymentintent_mock
291+
):
160292
self.add_second_owner()
161293
self.owner.delinquent = False
162294
self.owner.save()
163295
self.other_owner.delinquent = False
164296
self.other_owner.save()
165297

298+
retrieve_paymentintent_mock.return_value = MockPaymentIntent()
299+
166300
response = self._send_event(
167301
payload={
168302
"type": "invoice.payment_failed",
169303
"data": {
170304
"object": {
171305
"customer": self.owner.stripe_customer_id,
172306
"subscription": self.owner.stripe_subscription_id,
173-
"default_payment_method": {
174-
"card": {"brand": "visa", "last4": 1234}
175-
},
176307
"total": 24000,
177308
"hosted_invoice_url": "https://stripe.com",
309+
"payment_intent": "payment_intent_asdf",
178310
}
179311
},
180312
}
@@ -187,7 +319,12 @@ def test_invoice_payment_failed_sets_multiple_owners_delinquent_true(self):
187319
assert self.other_owner.delinquent is True
188320

189321
@patch("services.task.TaskService.send_email")
190-
def test_invoice_payment_failed_sends_email_to_admins(self, mocked_send_email):
322+
@patch("services.billing.stripe.PaymentIntent.retrieve")
323+
def test_invoice_payment_failed_sends_email_to_admins(
324+
self,
325+
retrieve_paymentintent_mock,
326+
mocked_send_email,
327+
):
191328
non_admin = OwnerFactory(email="[email protected]")
192329
admin_1 = OwnerFactory(email="[email protected]")
193330
admin_2 = OwnerFactory(email="[email protected]")
@@ -196,18 +333,18 @@ def test_invoice_payment_failed_sends_email_to_admins(self, mocked_send_email):
196333
self.owner.email = "[email protected]"
197334
self.owner.save()
198335

336+
retrieve_paymentintent_mock.return_value = MockPaymentIntent()
337+
199338
response = self._send_event(
200339
payload={
201340
"type": "invoice.payment_failed",
202341
"data": {
203342
"object": {
204343
"customer": self.owner.stripe_customer_id,
205344
"subscription": self.owner.stripe_subscription_id,
206-
"default_payment_method": {
207-
"card": {"brand": "visa", "last4": 1234}
208-
},
209345
"total": 24000,
210346
"hosted_invoice_url": "https://stripe.com",
347+
"payment_intent": "payment_intent_asdf",
211348
}
212349
},
213350
}
@@ -225,7 +362,7 @@ def test_invoice_payment_failed_sends_email_to_admins(self, mocked_send_email):
225362
name=self.owner.username,
226363
amount=240,
227364
card_type="visa",
228-
last_four=1234,
365+
last_four="1234",
229366
cta_link="https://stripe.com",
230367
date=datetime.now().strftime("%B %-d, %Y"),
231368
),
@@ -236,7 +373,7 @@ def test_invoice_payment_failed_sends_email_to_admins(self, mocked_send_email):
236373
name=admin_1.username,
237374
amount=240,
238375
card_type="visa",
239-
last_four=1234,
376+
last_four="1234",
240377
cta_link="https://stripe.com",
241378
date=datetime.now().strftime("%B %-d, %Y"),
242379
),
@@ -247,16 +384,20 @@ def test_invoice_payment_failed_sends_email_to_admins(self, mocked_send_email):
247384
name=admin_2.username,
248385
amount=240,
249386
card_type="visa",
250-
last_four=1234,
387+
last_four="1234",
251388
cta_link="https://stripe.com",
252389
date=datetime.now().strftime("%B %-d, %Y"),
253390
),
254391
]
392+
255393
mocked_send_email.assert_has_calls(expected_calls)
256394

257395
@patch("services.task.TaskService.send_email")
396+
@patch("services.billing.stripe.PaymentIntent.retrieve")
258397
def test_invoice_payment_failed_sends_email_to_admins_no_card(
259-
self, mocked_send_email
398+
self,
399+
retrieve_paymentintent_mock,
400+
mocked_send_email,
260401
):
261402
non_admin = OwnerFactory(email="[email protected]")
262403
admin_1 = OwnerFactory(email="[email protected]")
@@ -266,6 +407,8 @@ def test_invoice_payment_failed_sends_email_to_admins_no_card(
266407
self.owner.email = "[email protected]"
267408
self.owner.save()
268409

410+
retrieve_paymentintent_mock.return_value = MockPaymentIntent(noCard=True)
411+
269412
response = self._send_event(
270413
payload={
271414
"type": "invoice.payment_failed",
@@ -276,6 +419,7 @@ def test_invoice_payment_failed_sends_email_to_admins_no_card(
276419
"default_payment_method": None,
277420
"total": 24000,
278421
"hosted_invoice_url": "https://stripe.com",
422+
"payment_intent": "payment_intent_asdf",
279423
}
280424
},
281425
}

billing/views.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,42 @@ def invoice_payment_succeeded(self, invoice: stripe.Invoice) -> None:
4646
owners: QuerySet[Owner] = Owner.objects.filter(
4747
stripe_customer_id=invoice.customer,
4848
stripe_subscription_id=invoice.subscription,
49+
delinquent=True,
4950
)
50-
owners.update(delinquent=False)
5151

52+
if not owners.exists():
53+
return
54+
55+
admins = get_all_admins_for_owners(owners)
56+
owners.update(delinquent=False)
5257
self._log_updated(list(owners))
5358

59+
# Send a success email to all admins
60+
61+
task_service = TaskService()
62+
template_vars = {
63+
"amount": invoice.total / 100,
64+
"date": datetime.now().strftime("%B %-d, %Y"),
65+
"cta_link": invoice.hosted_invoice_url,
66+
}
67+
68+
for admin in admins:
69+
if admin.email:
70+
task_service.send_email(
71+
to_addr=admin.email,
72+
subject="You're all set",
73+
template_name="success-after-failed-payment",
74+
**template_vars,
75+
)
76+
77+
# temporary just making sure these look okay in the real world
78+
task_service.send_email(
79+
to_addr="[email protected]",
80+
subject="You're all set",
81+
template_name="success-after-failed-payment",
82+
**template_vars,
83+
)
84+
5485
def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
5586
log.info(
5687
"Invoice Payment Failed - Setting Delinquency status True",
@@ -70,9 +101,13 @@ def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
70101
admins = get_all_admins_for_owners(owners)
71102

72103
task_service = TaskService()
104+
payment_intent = stripe.PaymentIntent.retrieve(
105+
invoice["payment_intent"], expand=["payment_method"]
106+
)
73107
card = (
74-
invoice.default_payment_method.card
75-
if invoice.default_payment_method
108+
payment_intent.payment_method.card
109+
if payment_intent.payment_method
110+
and not isinstance(payment_intent.payment_method, str)
76111
else None
77112
)
78113
template_vars = {

graphql_api/tests/test_owner.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -678,18 +678,18 @@ def test_owner_available_plans(self):
678678
query = """{
679679
owner(username: "%s") {
680680
availablePlans {
681-
planName
681+
value
682682
}
683683
}
684684
}
685685
""" % (current_org.username)
686686
data = self.gql_request(query, owner=current_org)
687687
assert data["owner"]["availablePlans"] == [
688-
{"planName": "users-basic"},
689-
{"planName": "users-pr-inappm"},
690-
{"planName": "users-pr-inappy"},
691-
{"planName": "users-teamm"},
692-
{"planName": "users-teamy"},
688+
{"value": "users-basic"},
689+
{"value": "users-pr-inappm"},
690+
{"value": "users-pr-inappy"},
691+
{"value": "users-teamm"},
692+
{"value": "users-teamy"},
693693
]
694694

695695
def test_owner_query_with_no_service(self):

0 commit comments

Comments
 (0)