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

Commit 4327e66

Browse files
add tests
1 parent 262e1c3 commit 4327e66

File tree

4 files changed

+422
-13
lines changed

4 files changed

+422
-13
lines changed

billing/tests/test_views.py

Lines changed: 224 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import time
22
from datetime import datetime
3-
from unittest.mock import call, patch
3+
from unittest.mock import Mock, call, patch
44

55
import stripe
66
from django.conf import settings
@@ -12,6 +12,7 @@
1212
from shared.plan.constants import PlanName
1313

1414
from billing.helpers import mock_all_plans_and_tiers
15+
from billing.views import StripeWebhookHandler
1516

1617
from ..constants import StripeHTTPHeaders
1718

@@ -267,6 +268,40 @@ def test_invoice_payment_succeeded_emails_delinquents(self, mocked_send_email):
267268

268269
mocked_send_email.assert_has_calls(expected_calls)
269270

271+
@patch("services.billing.stripe.PaymentIntent.retrieve")
272+
def test_invoice_payment_failed_skips_delinquency_if_payment_intent_requires_action(
273+
self, retrieve_paymentintent_mock
274+
):
275+
self.owner.delinquent = False
276+
self.owner.save()
277+
278+
class MockPaymentIntentRequiresAction:
279+
status = "requires_action"
280+
next_action = {"type": "verify_with_microdeposits"}
281+
282+
retrieve_paymentintent_mock.return_value = MockPaymentIntentRequiresAction()
283+
284+
response = self._send_event(
285+
payload={
286+
"type": "invoice.payment_failed",
287+
"data": {
288+
"object": {
289+
"customer": self.owner.stripe_customer_id,
290+
"subscription": self.owner.stripe_subscription_id,
291+
"total": 24000,
292+
"hosted_invoice_url": "https://stripe.com",
293+
"payment_intent": "payment_intent_asdf",
294+
"default_payment_method": None,
295+
}
296+
},
297+
}
298+
)
299+
300+
self.owner.refresh_from_db()
301+
assert response.status_code == status.HTTP_204_NO_CONTENT
302+
assert self.owner.delinquent is False
303+
retrieve_paymentintent_mock.assert_called_once_with("payment_intent_asdf")
304+
270305
@patch("services.billing.stripe.PaymentIntent.retrieve")
271306
def test_invoice_payment_failed_sets_owner_delinquent_true(
272307
self, retrieve_paymentintent_mock
@@ -652,6 +687,40 @@ def test_customer_created_logs_and_doesnt_crash(self):
652687
}
653688
)
654689

690+
@patch("billing.views.StripeWebhookHandler._has_unverified_initial_payment_method")
691+
def test_customer_subscription_created_early_returns_if_unverified_payment(
692+
self, mock_has_unverified
693+
):
694+
mock_has_unverified.return_value = True
695+
self.owner.stripe_subscription_id = None
696+
self.owner.stripe_customer_id = None
697+
self.owner.plan = "users-basic"
698+
self.owner.save()
699+
700+
response = self._send_event(
701+
payload={
702+
"type": "customer.subscription.created",
703+
"data": {
704+
"object": {
705+
"id": "sub_123",
706+
"customer": "cus_123",
707+
"plan": {"id": "plan_H6P16wij3lUuxg"},
708+
"metadata": {"obo_organization": self.owner.ownerid},
709+
"quantity": 20,
710+
}
711+
},
712+
}
713+
)
714+
715+
self.owner.refresh_from_db()
716+
assert response.status_code == status.HTTP_204_NO_CONTENT
717+
# Subscription and customer IDs should be set
718+
assert self.owner.stripe_subscription_id == "sub_123"
719+
assert self.owner.stripe_customer_id == "cus_123"
720+
# But plan should not be updated since payment is unverified
721+
assert self.owner.plan == "users-basic"
722+
mock_has_unverified.assert_called_once()
723+
655724
def test_customer_subscription_created_does_nothing_if_no_plan_id(self):
656725
self.owner.stripe_subscription_id = None
657726
self.owner.stripe_customer_id = None
@@ -1448,3 +1517,157 @@ def test_customer_update_payment_method(self, subscription_modify_mock):
14481517
subscription_modify_mock.assert_called_once_with(
14491518
"sub_123", default_payment_method=payment_method
14501519
)
1520+
1521+
@patch("services.billing.stripe.PaymentIntent.retrieve")
1522+
@patch("services.billing.stripe.Invoice.retrieve")
1523+
def test_has_unverified_initial_payment_method(
1524+
self, invoice_retrieve_mock, payment_intent_retrieve_mock
1525+
):
1526+
subscription = Mock()
1527+
subscription.latest_invoice = "inv_123"
1528+
1529+
class MockPaymentIntent:
1530+
status = "requires_action"
1531+
1532+
invoice_retrieve_mock.return_value = Mock(payment_intent="pi_123")
1533+
payment_intent_retrieve_mock.return_value = MockPaymentIntent()
1534+
1535+
handler = StripeWebhookHandler()
1536+
result = handler._has_unverified_initial_payment_method(subscription)
1537+
1538+
assert result is True
1539+
invoice_retrieve_mock.assert_called_once_with("inv_123")
1540+
payment_intent_retrieve_mock.assert_called_once_with("pi_123")
1541+
1542+
@patch("services.billing.stripe.PaymentIntent.retrieve")
1543+
@patch("services.billing.stripe.Invoice.retrieve")
1544+
def test_has_unverified_initial_payment_method_no_payment_intent(
1545+
self, invoice_retrieve_mock, payment_intent_retrieve_mock
1546+
):
1547+
subscription = Mock()
1548+
subscription.latest_invoice = "inv_123"
1549+
1550+
invoice_retrieve_mock.return_value = Mock(payment_intent=None)
1551+
1552+
handler = StripeWebhookHandler()
1553+
result = handler._has_unverified_initial_payment_method(subscription)
1554+
1555+
assert result is False
1556+
invoice_retrieve_mock.assert_called_once_with("inv_123")
1557+
payment_intent_retrieve_mock.assert_not_called()
1558+
1559+
@patch("services.billing.stripe.PaymentIntent.retrieve")
1560+
@patch("services.billing.stripe.Invoice.retrieve")
1561+
def test_has_unverified_initial_payment_method_payment_intent_succeeded(
1562+
self, invoice_retrieve_mock, payment_intent_retrieve_mock
1563+
):
1564+
subscription = Mock()
1565+
subscription.latest_invoice = "inv_123"
1566+
1567+
class MockPaymentIntent:
1568+
status = "succeeded"
1569+
1570+
invoice_retrieve_mock.return_value = Mock(payment_intent="pi_123")
1571+
payment_intent_retrieve_mock.return_value = MockPaymentIntent()
1572+
1573+
handler = StripeWebhookHandler()
1574+
result = handler._has_unverified_initial_payment_method(subscription)
1575+
1576+
assert result is False
1577+
invoice_retrieve_mock.assert_called_once_with("inv_123")
1578+
payment_intent_retrieve_mock.assert_called_once_with("pi_123")
1579+
1580+
@patch("services.billing.stripe.PaymentMethod.attach")
1581+
@patch("services.billing.stripe.Customer.modify")
1582+
@patch("services.billing.stripe.Subscription.modify")
1583+
@patch("services.billing.stripe.PaymentMethod.retrieve")
1584+
def test_check_and_handle_delayed_notification_payment_methods(
1585+
self,
1586+
payment_method_retrieve_mock,
1587+
subscription_modify_mock,
1588+
customer_modify_mock,
1589+
payment_method_attach_mock,
1590+
):
1591+
class MockPaymentMethod:
1592+
type = "us_bank_account"
1593+
us_bank_account = {}
1594+
id = "pm_123"
1595+
1596+
payment_method_retrieve_mock.return_value = MockPaymentMethod()
1597+
1598+
self.owner.stripe_subscription_id = "sub_123"
1599+
self.owner.stripe_customer_id = "cus_123"
1600+
self.owner.save()
1601+
1602+
handler = StripeWebhookHandler()
1603+
handler._check_and_handle_delayed_notification_payment_methods(
1604+
"cus_123", "pm_123"
1605+
)
1606+
1607+
payment_method_retrieve_mock.assert_called_once_with("pm_123")
1608+
payment_method_attach_mock.assert_called_once_with(
1609+
payment_method_retrieve_mock.return_value, customer="cus_123"
1610+
)
1611+
customer_modify_mock.assert_called_once_with(
1612+
"cus_123",
1613+
invoice_settings={
1614+
"default_payment_method": payment_method_retrieve_mock.return_value
1615+
},
1616+
)
1617+
subscription_modify_mock.assert_called_once_with(
1618+
"sub_123", default_payment_method=payment_method_retrieve_mock.return_value
1619+
)
1620+
1621+
@patch(
1622+
"billing.views.StripeWebhookHandler._check_and_handle_delayed_notification_payment_methods"
1623+
)
1624+
@patch("logging.Logger.info")
1625+
def test_payment_intent_succeeded(
1626+
self, log_info_mock, check_and_handle_delayed_notification_mock
1627+
):
1628+
class MockPaymentIntent:
1629+
id = "pi_123"
1630+
customer = "cus_123"
1631+
payment_method = "pm_123"
1632+
1633+
handler = StripeWebhookHandler()
1634+
handler.payment_intent_succeeded(MockPaymentIntent())
1635+
1636+
check_and_handle_delayed_notification_mock.assert_called_once_with(
1637+
"cus_123", "pm_123"
1638+
)
1639+
log_info_mock.assert_called_once_with(
1640+
"Payment intent succeeded",
1641+
extra=dict(
1642+
stripe_customer_id="cus_123",
1643+
payment_intent_id="pi_123",
1644+
payment_method_type="pm_123",
1645+
),
1646+
)
1647+
1648+
@patch(
1649+
"billing.views.StripeWebhookHandler._check_and_handle_delayed_notification_payment_methods"
1650+
)
1651+
@patch("logging.Logger.info")
1652+
def test_setup_intent_succeeded(
1653+
self, log_info_mock, check_and_handle_delayed_notification_mock
1654+
):
1655+
class MockSetupIntent:
1656+
id = "seti_123"
1657+
customer = "cus_123"
1658+
payment_method = "pm_123"
1659+
1660+
handler = StripeWebhookHandler()
1661+
handler.setup_intent_succeeded(MockSetupIntent())
1662+
1663+
check_and_handle_delayed_notification_mock.assert_called_once_with(
1664+
"cus_123", "pm_123"
1665+
)
1666+
log_info_mock.assert_called_once_with(
1667+
"Setup intent succeeded",
1668+
extra=dict(
1669+
stripe_customer_id="cus_123",
1670+
setup_intent_id="seti_123",
1671+
payment_method_type="pm_123",
1672+
),
1673+
)

billing/views.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -164,21 +164,12 @@ def customer_subscription_deleted(self, subscription: stripe.Subscription) -> No
164164
or when cleaning up an incomplete subscription that never activated (e.g., abandoned async
165165
ACH microdeposits verification).
166166
"""
167-
if subscription.status == "incomplete":
168-
log.info(
169-
"Customer Subscription Deleted - Ignoring incomplete subscription",
170-
extra=dict(
171-
stripe_subscription_id=subscription.id,
172-
stripe_customer_id=subscription.customer,
173-
),
174-
)
175-
return
176-
177167
log.info(
178168
"Customer Subscription Deleted - Setting free plan and deactivating repos for stripe customer",
179169
extra=dict(
180170
stripe_subscription_id=subscription.id,
181171
stripe_customer_id=subscription.customer,
172+
previous_subscription_status=subscription.status,
182173
),
183174
)
184175
owners: QuerySet[Owner] = Owner.objects.filter(
@@ -335,6 +326,8 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No
335326
owner.stripe_customer_id = subscription.customer
336327
owner.save()
337328

329+
# We may reach here if the subscription was created with a payment method
330+
# that is awaiting verification (e.g. ACH microdeposits)
338331
if self._has_unverified_initial_payment_method(subscription):
339332
log.info(
340333
"Subscription has pending initial payment verification - will upgrade plan after initial invoice payment",

services/billing.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,8 +1014,6 @@ def _cleanup_incomplete_subscription(
10141014
payment_intent_id=payment_intent.id,
10151015
),
10161016
)
1017-
owner.stripe_subscription_id = None
1018-
owner.save()
10191017
except Exception as e:
10201018
log.error(
10211019
"Failed to delete subscription",

0 commit comments

Comments
 (0)