Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 160 additions & 16 deletions billing/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,35 @@ def __getitem__(self, key):
return getattr(self, key)


class MockCard(object):
def __init__(self):
self.brand = "visa"
self.last4 = "1234"

def __getitem__(self, key):
return getattr(self, key)


class MockPaymentMethod(object):
def __init__(self, noCard=False):
if noCard:
self.card = None
return

self.card = MockCard()

def __getitem__(self, key):
return getattr(self, key)


class MockPaymentIntent(object):
def __init__(self, noCard=False):
self.payment_method = MockPaymentMethod(noCard)

def __getitem__(self, key):
return getattr(self, key)


class StripeWebhookHandlerTests(APITestCase):
def setUp(self):
self.owner = OwnerFactory(
Expand Down Expand Up @@ -97,6 +126,8 @@ def test_invoice_payment_succeeded_sets_owner_delinquent_false(self):
"object": {
"customer": self.owner.stripe_customer_id,
"subscription": self.owner.stripe_subscription_id,
"total": 24000,
"hosted_invoice_url": "https://stripe.com",
}
},
}
Expand All @@ -120,6 +151,72 @@ def test_invoice_payment_succeeded_sets_multiple_owners_delinquent_false(self):
"object": {
"customer": self.owner.stripe_customer_id,
"subscription": self.owner.stripe_subscription_id,
"total": 24000,
"hosted_invoice_url": "https://stripe.com",
}
},
}
)

self.owner.refresh_from_db()
self.other_owner.refresh_from_db()
assert response.status_code == status.HTTP_204_NO_CONTENT
assert self.owner.delinquent is False
assert self.other_owner.delinquent is False

@patch("services.task.TaskService.send_email")
def test_invoice_payment_succeeded_emails_only_emails_delinquents(
self,
mocked_send_email,
):
self.add_second_owner()
self.owner.delinquent = False
self.owner.save()

response = self._send_event(
payload={
"type": "invoice.payment_succeeded",
"data": {
"object": {
"customer": self.owner.stripe_customer_id,
"subscription": self.owner.stripe_subscription_id,
"total": 24000,
"hosted_invoice_url": "https://stripe.com",
}
},
}
)

self.owner.refresh_from_db()
self.other_owner.refresh_from_db()
assert response.status_code == status.HTTP_204_NO_CONTENT
assert self.owner.delinquent is False

mocked_send_email.assert_not_called()

@patch("services.task.TaskService.send_email")
def test_invoice_payment_succeeded_emails_delinquents(self, mocked_send_email):
non_admin = OwnerFactory(email="[email protected]")
admin_1 = OwnerFactory(email="[email protected]")
admin_2 = OwnerFactory(email="[email protected]")
self.owner.admins = [admin_1.ownerid, admin_2.ownerid]
self.owner.plan_activated_users = [non_admin.ownerid]
self.owner.email = "[email protected]"
self.owner.delinquent = True
self.owner.save()
self.add_second_owner()
self.other_owner.delinquent = False
self.other_owner.save()

response = self._send_event(
payload={
"type": "invoice.payment_succeeded",
"data": {
"object": {
"customer": self.owner.stripe_customer_id,
"subscription": self.owner.stripe_subscription_id,
"total": 24000,
"hosted_invoice_url": "https://stripe.com",
}
},
}
Expand All @@ -131,22 +228,54 @@ def test_invoice_payment_succeeded_sets_multiple_owners_delinquent_false(self):
assert self.owner.delinquent is False
assert self.other_owner.delinquent is False

def test_invoice_payment_failed_sets_owner_delinquent_true(self):
expected_calls = [
call(
to_addr=self.owner.email,
subject="You're all set",
template_name="success-after-failed-payment",
amount=240,
cta_link="https://stripe.com",
date=datetime.now().strftime("%B %-d, %Y"),
),
call(
to_addr=admin_1.email,
subject="You're all set",
template_name="success-after-failed-payment",
amount=240,
cta_link="https://stripe.com",
date=datetime.now().strftime("%B %-d, %Y"),
),
call(
to_addr=admin_2.email,
subject="You're all set",
template_name="success-after-failed-payment",
amount=240,
cta_link="https://stripe.com",
date=datetime.now().strftime("%B %-d, %Y"),
),
]

mocked_send_email.assert_has_calls(expected_calls)

@patch("services.billing.stripe.PaymentIntent.retrieve")
def test_invoice_payment_failed_sets_owner_delinquent_true(
self, retrieve_paymentintent_mock
):
self.owner.delinquent = False
self.owner.save()

retrieve_paymentintent_mock.return_value = MockPaymentIntent()

response = self._send_event(
payload={
"type": "invoice.payment_failed",
"data": {
"object": {
"customer": self.owner.stripe_customer_id,
"subscription": self.owner.stripe_subscription_id,
"default_payment_method": {
"card": {"brand": "visa", "last4": 1234}
},
"total": 24000,
"hosted_invoice_url": "https://stripe.com",
"payment_intent": "payment_intent_asdf",
}
},
}
Expand All @@ -156,25 +285,28 @@ def test_invoice_payment_failed_sets_owner_delinquent_true(self):
assert response.status_code == status.HTTP_204_NO_CONTENT
assert self.owner.delinquent is True

def test_invoice_payment_failed_sets_multiple_owners_delinquent_true(self):
@patch("services.billing.stripe.PaymentIntent.retrieve")
def test_invoice_payment_failed_sets_multiple_owners_delinquent_true(
self, retrieve_paymentintent_mock
):
self.add_second_owner()
self.owner.delinquent = False
self.owner.save()
self.other_owner.delinquent = False
self.other_owner.save()

retrieve_paymentintent_mock.return_value = MockPaymentIntent()

response = self._send_event(
payload={
"type": "invoice.payment_failed",
"data": {
"object": {
"customer": self.owner.stripe_customer_id,
"subscription": self.owner.stripe_subscription_id,
"default_payment_method": {
"card": {"brand": "visa", "last4": 1234}
},
"total": 24000,
"hosted_invoice_url": "https://stripe.com",
"payment_intent": "payment_intent_asdf",
}
},
}
Expand All @@ -187,7 +319,12 @@ def test_invoice_payment_failed_sets_multiple_owners_delinquent_true(self):
assert self.other_owner.delinquent is True

@patch("services.task.TaskService.send_email")
def test_invoice_payment_failed_sends_email_to_admins(self, mocked_send_email):
@patch("services.billing.stripe.PaymentIntent.retrieve")
def test_invoice_payment_failed_sends_email_to_admins(
self,
retrieve_paymentintent_mock,
mocked_send_email,
):
non_admin = OwnerFactory(email="[email protected]")
admin_1 = OwnerFactory(email="[email protected]")
admin_2 = OwnerFactory(email="[email protected]")
Expand All @@ -196,18 +333,18 @@ def test_invoice_payment_failed_sends_email_to_admins(self, mocked_send_email):
self.owner.email = "[email protected]"
self.owner.save()

retrieve_paymentintent_mock.return_value = MockPaymentIntent()

response = self._send_event(
payload={
"type": "invoice.payment_failed",
"data": {
"object": {
"customer": self.owner.stripe_customer_id,
"subscription": self.owner.stripe_subscription_id,
"default_payment_method": {
"card": {"brand": "visa", "last4": 1234}
},
"total": 24000,
"hosted_invoice_url": "https://stripe.com",
"payment_intent": "payment_intent_asdf",
}
},
}
Expand All @@ -225,7 +362,7 @@ def test_invoice_payment_failed_sends_email_to_admins(self, mocked_send_email):
name=self.owner.username,
amount=240,
card_type="visa",
last_four=1234,
last_four="1234",
cta_link="https://stripe.com",
date=datetime.now().strftime("%B %-d, %Y"),
),
Expand All @@ -236,7 +373,7 @@ def test_invoice_payment_failed_sends_email_to_admins(self, mocked_send_email):
name=admin_1.username,
amount=240,
card_type="visa",
last_four=1234,
last_four="1234",
cta_link="https://stripe.com",
date=datetime.now().strftime("%B %-d, %Y"),
),
Expand All @@ -247,16 +384,20 @@ def test_invoice_payment_failed_sends_email_to_admins(self, mocked_send_email):
name=admin_2.username,
amount=240,
card_type="visa",
last_four=1234,
last_four="1234",
cta_link="https://stripe.com",
date=datetime.now().strftime("%B %-d, %Y"),
),
]

mocked_send_email.assert_has_calls(expected_calls)

@patch("services.task.TaskService.send_email")
@patch("services.billing.stripe.PaymentIntent.retrieve")
def test_invoice_payment_failed_sends_email_to_admins_no_card(
self, mocked_send_email
self,
retrieve_paymentintent_mock,
mocked_send_email,
):
non_admin = OwnerFactory(email="[email protected]")
admin_1 = OwnerFactory(email="[email protected]")
Expand All @@ -266,6 +407,8 @@ def test_invoice_payment_failed_sends_email_to_admins_no_card(
self.owner.email = "[email protected]"
self.owner.save()

retrieve_paymentintent_mock.return_value = MockPaymentIntent(noCard=True)

response = self._send_event(
payload={
"type": "invoice.payment_failed",
Expand All @@ -276,6 +419,7 @@ def test_invoice_payment_failed_sends_email_to_admins_no_card(
"default_payment_method": None,
"total": 24000,
"hosted_invoice_url": "https://stripe.com",
"payment_intent": "payment_intent_asdf",
}
},
}
Expand Down
41 changes: 38 additions & 3 deletions billing/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,42 @@ def invoice_payment_succeeded(self, invoice: stripe.Invoice) -> None:
owners: QuerySet[Owner] = Owner.objects.filter(
stripe_customer_id=invoice.customer,
stripe_subscription_id=invoice.subscription,
delinquent=True,
)
owners.update(delinquent=False)

if not owners.exists():
return

admins = get_all_admins_for_owners(owners)
owners.update(delinquent=False)
self._log_updated(list(owners))

# Send a success email to all admins

task_service = TaskService()
template_vars = {
"amount": invoice.total / 100,
"date": datetime.now().strftime("%B %-d, %Y"),
"cta_link": invoice.hosted_invoice_url,
}

for admin in admins:
if admin.email:
task_service.send_email(
to_addr=admin.email,
subject="You're all set",
template_name="success-after-failed-payment",
**template_vars,
)

# temporary just making sure these look okay in the real world
task_service.send_email(
to_addr="[email protected]",
subject="You're all set",
template_name="success-after-failed-payment",
**template_vars,
)

def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
log.info(
"Invoice Payment Failed - Setting Delinquency status True",
Expand All @@ -70,9 +101,13 @@ def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
admins = get_all_admins_for_owners(owners)

task_service = TaskService()
payment_intent = stripe.PaymentIntent.retrieve(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Turns out you gotta go get the card in the PaymentIntent explicitly. Annoying that it's another fetch, but oh well, this code isn't time critical enough for that to matter.

invoice["payment_intent"], expand=["payment_method"]
)
card = (
invoice.default_payment_method.card
if invoice.default_payment_method
payment_intent.payment_method.card
if payment_intent.payment_method
and not isinstance(payment_intent.payment_method, str)
else None
)
template_vars = {
Expand Down
Loading