|
1 | 1 | import time |
2 | 2 | from datetime import datetime |
3 | | -from unittest.mock import call, patch |
| 3 | +from unittest.mock import Mock, call, patch |
4 | 4 |
|
5 | 5 | import stripe |
6 | 6 | from django.conf import settings |
|
12 | 12 | from shared.plan.constants import PlanName |
13 | 13 |
|
14 | 14 | from billing.helpers import mock_all_plans_and_tiers |
| 15 | +from billing.views import StripeWebhookHandler |
15 | 16 |
|
16 | 17 | from ..constants import StripeHTTPHeaders |
17 | 18 |
|
@@ -267,6 +268,40 @@ def test_invoice_payment_succeeded_emails_delinquents(self, mocked_send_email): |
267 | 268 |
|
268 | 269 | mocked_send_email.assert_has_calls(expected_calls) |
269 | 270 |
|
| 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 | + |
270 | 305 | @patch("services.billing.stripe.PaymentIntent.retrieve") |
271 | 306 | def test_invoice_payment_failed_sets_owner_delinquent_true( |
272 | 307 | self, retrieve_paymentintent_mock |
@@ -652,6 +687,40 @@ def test_customer_created_logs_and_doesnt_crash(self): |
652 | 687 | } |
653 | 688 | ) |
654 | 689 |
|
| 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 | + |
655 | 724 | def test_customer_subscription_created_does_nothing_if_no_plan_id(self): |
656 | 725 | self.owner.stripe_subscription_id = None |
657 | 726 | self.owner.stripe_customer_id = None |
@@ -1448,3 +1517,157 @@ def test_customer_update_payment_method(self, subscription_modify_mock): |
1448 | 1517 | subscription_modify_mock.assert_called_once_with( |
1449 | 1518 | "sub_123", default_payment_method=payment_method |
1450 | 1519 | ) |
| 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 | + ) |
0 commit comments