diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 87c7928ceb..882e16f00a 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -103,29 +103,6 @@ def stripe_get_customer(stripe_customer_id: str) -> stripe.Customer: def stripe_get_upcoming_invoice(stripe_customer_id: str) -> stripe.Invoice: return stripe.Invoice.upcoming(customer=stripe_customer_id) -@catch_stripe_errors -def stripe_get_invoice_preview_for_downgrade( - stripe_customer_id: str, stripe_subscription_id: str, - stripe_subscriptionitem_id: str) -> stripe.Invoice: - return stripe.Invoice.upcoming( - customer=stripe_customer_id, subscription=stripe_subscription_id, - subscription_items=[{'id': stripe_subscriptionitem_id, 'quantity': 0}]) - -def preview_invoice_total_for_downgrade(stripe_customer: stripe.Customer) -> int: - stripe_subscription = extract_current_subscription(stripe_customer) - if stripe_subscription is None: - # Most likely situation is: user A goes to billing page, user B - # cancels subscription, user A clicks on "downgrade" or something - # else that calls this function. - billing_logger.error("Trying to extract subscription item that doesn't exist, for Stripe customer %s" - % (stripe_customer.id,)) - raise BillingError('downgrade without subscription', BillingError.TRY_RELOADING) - for item in stripe_subscription['items']: - # There should only be one item, but we can't index into stripe_subscription['items'] - stripe_subscriptionitem_id = item.id - return stripe_get_invoice_preview_for_downgrade( - stripe_customer.id, stripe_subscription.id, stripe_subscriptionitem_id).total - # This allows us to access /billing in tests without having to mock the # whole invoice object def upcoming_invoice_total(stripe_customer_id: str) -> int: @@ -289,34 +266,5 @@ def attach_discount_to_realm(user: UserProfile, percent_off: int) -> None: else: do_replace_coupon(user, coupon) -@catch_stripe_errors -def process_downgrade(user: UserProfile) -> None: - # This is no longer implementing our intended policy. Until that is - # resolved we should use the Stripe dashboard to downgrade. - assert(settings.TEST_SUITE) - stripe_customer = stripe_get_customer( - Customer.objects.filter(realm=user.realm).first().stripe_customer_id) - subscription_balance = preview_invoice_total_for_downgrade(stripe_customer) - # If subscription_balance > 0, they owe us money. This is likely due to - # people they added in the last day, so we can just forgive it. - # Stripe automatically forgives it when we delete the subscription, so nothing we need to do there. - if subscription_balance < 0: - stripe_customer.account_balance = stripe_customer.account_balance + subscription_balance - stripe_subscription = extract_current_subscription(stripe_customer) - # Wish these two could be transaction.atomic - stripe_subscription = stripe.Subscription.delete(stripe_subscription) - stripe.Customer.save(stripe_customer) - with transaction.atomic(): - user.realm.has_seat_based_plan = False - user.realm.save(update_fields=['has_seat_based_plan']) - RealmAuditLog.objects.create( - realm=user.realm, - acting_user=user, - event_type=RealmAuditLog.STRIPE_PLAN_CHANGED, - event_time=timestamp_to_datetime(stripe_subscription.canceled_at), - extra_data=ujson.dumps({'plan': None, 'quantity': stripe_subscription.quantity})) - # Doing this last, since it results in user-visible confirmation (via - # product changes) that the downgrade succeeded. - # Keeping it out of the transaction.atomic block because it will - # eventually have a lot of stuff going on. - do_change_plan_type(user.realm, Realm.LIMITED) +def process_downgrade(user: UserProfile) -> None: # nocoverage + pass diff --git a/corporate/tests/stripe_fixtures/downgrade:Customer.create.1.json b/corporate/tests/stripe_fixtures/downgrade:Customer.create.1.json deleted file mode 100644 index e244e1335a..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade:Customer.create.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade:Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/downgrade:Customer.retrieve.1.json deleted file mode 100644 index 1436a0a95e..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade:Customer.retrieve.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade:Customer.retrieve.2.json b/corporate/tests/stripe_fixtures/downgrade:Customer.retrieve.2.json deleted file mode 100644 index 43d0bd99db..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade:Customer.retrieve.2.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade:Customer.retrieve.3.json b/corporate/tests/stripe_fixtures/downgrade:Customer.retrieve.3.json deleted file mode 100644 index f74c127032..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade:Customer.retrieve.3.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade:Customer.save.1.json b/corporate/tests/stripe_fixtures/downgrade:Customer.save.1.json deleted file mode 100644 index 9f79fa4026..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade:Customer.save.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade:Invoice.upcoming.1.json b/corporate/tests/stripe_fixtures/downgrade:Invoice.upcoming.1.json deleted file mode 100644 index 498b0d6b1f..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade:Invoice.upcoming.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade:Subscription.create.1.json b/corporate/tests/stripe_fixtures/downgrade:Subscription.create.1.json deleted file mode 100644 index 705c1038cf..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade:Subscription.create.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade:Subscription.delete.1.json b/corporate/tests/stripe_fixtures/downgrade:Subscription.delete.1.json deleted file mode 100644 index 6d51c37e70..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade:Subscription.delete.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade:Subscription.retrieve.1.json b/corporate/tests/stripe_fixtures/downgrade:Subscription.retrieve.1.json deleted file mode 100644 index 6d51c37e70..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade:Subscription.retrieve.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade:Subscription.save.1.json b/corporate/tests/stripe_fixtures/downgrade:Subscription.save.1.json deleted file mode 100644 index e2c4f94315..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade:Subscription.save.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade:Token.create.1.json b/corporate/tests/stripe_fixtures/downgrade:Token.create.1.json deleted file mode 100644 index 60d47536e7..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade:Token.create.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.create.1.json b/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.create.1.json deleted file mode 100644 index e244e1335a..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.create.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.retrieve.1.json deleted file mode 100644 index 1436a0a95e..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.retrieve.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.retrieve.2.json b/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.retrieve.2.json deleted file mode 100644 index 1436a0a95e..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.retrieve.2.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.retrieve.3.json b/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.retrieve.3.json deleted file mode 100644 index 81d5b5a99f..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.retrieve.3.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.save.1.json b/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.save.1.json deleted file mode 100644 index b8c8d9814d..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Customer.save.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Invoice.upcoming.1.json b/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Invoice.upcoming.1.json deleted file mode 100644 index 96b2e26d16..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Invoice.upcoming.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:InvoiceItem.create.1.json b/corporate/tests/stripe_fixtures/downgrade_with_money_owed:InvoiceItem.create.1.json deleted file mode 100644 index e5b41ca2ff..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:InvoiceItem.create.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Subscription.create.1.json b/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Subscription.create.1.json deleted file mode 100644 index 705c1038cf..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Subscription.create.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Subscription.delete.1.json b/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Subscription.delete.1.json deleted file mode 100644 index 93a4b8419b..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Subscription.delete.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Subscription.retrieve.1.json b/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Subscription.retrieve.1.json deleted file mode 100644 index 93a4b8419b..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Subscription.retrieve.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Token.create.1.json b/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Token.create.1.json deleted file mode 100644 index 60d47536e7..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade_with_money_owed:Token.create.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade_with_no_subscription:Customer.create.1.json b/corporate/tests/stripe_fixtures/downgrade_with_no_subscription:Customer.create.1.json deleted file mode 100644 index 49d7f16c2c..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade_with_no_subscription:Customer.create.1.json and /dev/null differ diff --git a/corporate/tests/stripe_fixtures/downgrade_with_no_subscription:Customer.retrieve.1.json b/corporate/tests/stripe_fixtures/downgrade_with_no_subscription:Customer.retrieve.1.json deleted file mode 100644 index 49d7f16c2c..0000000000 Binary files a/corporate/tests/stripe_fixtures/downgrade_with_no_subscription:Customer.retrieve.1.json and /dev/null differ diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index cf834d9143..071639d207 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -27,8 +27,7 @@ from corporate.lib.stripe import catch_stripe_errors, \ do_subscribe_customer_to_plan, attach_discount_to_realm, \ get_seat_count, extract_current_subscription, sign_string, unsign_string, \ BillingError, StripeCardError, StripeConnectionError, stripe_get_customer, \ - DEFAULT_INVOICE_DAYS_UNTIL_DUE, MIN_INVOICED_SEAT_COUNT, do_create_customer, \ - process_downgrade + DEFAULT_INVOICE_DAYS_UNTIL_DUE, MIN_INVOICED_SEAT_COUNT, do_create_customer from corporate.models import Customer, Plan, Coupon from corporate.views import payment_method_string import corporate.urls @@ -625,27 +624,11 @@ class StripeTest(ZulipTestCase): # If you signup with a card and then downgrade, we still have your # card on file, and should show it - realm = do_create_realm('realm1', 'realm1') - user = do_create_user('name@realm1.com', 'password', realm, 'name', 'name') - self.login(user.email, password='password', realm=realm) - self.upgrade(realm=realm) - with patch('corporate.lib.stripe.preview_invoice_total_for_downgrade', return_value=1): - process_downgrade(user) - stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) - self.assertEqual('Card ending in 4242', payment_method_string(stripe_customer)) + # TODO # If you signup via invoice, and then downgrade immediately, the # default_source is in a weird intermediate state. - realm = do_create_realm('realm2', 'realm2') - user = do_create_user('name@realm2.com', 'password', realm, 'name', 'name') - self.login(user.email, password='password', realm=realm) - self.upgrade(invoice=True, realm=realm) - with patch('corporate.lib.stripe.preview_invoice_total_for_downgrade', return_value=1): - process_downgrade(user) - stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) - # Could be either one, depending on exact timing with the test - self.assertTrue('Unknown payment method' in payment_method_string(stripe_customer) or - 'No payment method' in payment_method_string(stripe_customer)) + # TODO @mock_stripe() def test_attach_discount_to_realm(self, *mocks: Mock) -> None: @@ -673,86 +656,6 @@ class StripeTest(ZulipTestCase): upcoming_invoice = stripe.Invoice.upcoming(customer=stripe_customer.id) self.assertEqual(upcoming_invoice.amount_due, get_seat_count(user.realm) * 80 * 75) - # Tests upgrade followed by immediate downgrade. Doesn't test the - # calculations for how much credit they should get if they had the - # subscription for more than 0 time. - @mock_stripe(tested_timestamp_fields=["canceled_at"]) - def test_downgrade(self, *mocks: Mock) -> None: - user = self.example_user('iago') - self.login(user.email) - self.upgrade() - realm = get_realm('zulip') - self.assertEqual(realm.has_seat_based_plan, True) - self.assertEqual(realm.plan_type, Realm.STANDARD) - RealmAuditLog.objects.filter(realm=realm).delete() - - stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) - self.assertEqual(stripe_customer.account_balance, 0) - # Change subscription in Stripe, but don't pay for it - stripe_subscription = extract_current_subscription(stripe_customer) - stripe_subscription.quantity = 123 - stripe.Subscription.save(stripe_subscription) - - response = self.client_post("/json/billing/downgrade", {}) - self.assert_json_success(response) - stripe_customer = stripe_get_customer(stripe_customer.id) - self.assertEqual(stripe_customer.account_balance, self.seat_count * -8000) - self.assertIsNone(extract_current_subscription(stripe_customer)) - stripe_subscription = stripe.Subscription.retrieve(stripe_subscription.id) - self.assertEqual(stripe_subscription.status, "canceled") - - realm = get_realm('zulip') - self.assertEqual(realm.plan_type, Realm.LIMITED) - self.assertFalse(realm.has_seat_based_plan) - - audit_log_entries = list(RealmAuditLog.objects.filter(realm=realm) - .values_list('event_type', 'event_time', - 'requires_billing_update').order_by('id')) - self.assertEqual(audit_log_entries, [ - (RealmAuditLog.STRIPE_PLAN_CHANGED, - timestamp_to_datetime(stripe_subscription.canceled_at), False), - (RealmAuditLog.REALM_PLAN_TYPE_CHANGED, Kandra(), False), - ]) - self.assertEqual(ujson.loads(RealmAuditLog.objects.filter( - event_type=RealmAuditLog.STRIPE_PLAN_CHANGED).values_list('extra_data', flat=True).first()), - {'plan': None, 'quantity': 123}) - - @mock_stripe() - def test_downgrade_with_no_subscription(self, *mocks: Mock) -> None: - user = self.example_user("iago") - do_create_customer(user) - self.login(user.email) - with patch("stripe.Customer.save") as mock_save_customer: - with patch("corporate.lib.stripe.billing_logger.error"): - response = self.client_post("/json/billing/downgrade", {}) - mock_save_customer.assert_not_called() - self.assert_json_error_contains(response, 'Please reload') - self.assertEqual(ujson.loads(response.content)['error_description'], 'downgrade without subscription') - - @mock_stripe() - def test_downgrade_with_money_owed(self, *mocks: Mock) -> None: - user = self.example_user('iago') - self.login(user.email) - self.upgrade() - stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id) - self.assertEqual(stripe_customer.account_balance, 0) - stripe_subscription = extract_current_subscription(stripe_customer) - # Create a situation where customer net owes us money - stripe.InvoiceItem.create( - amount=100000, - currency='usd', - customer=stripe_customer, - subscription=stripe_subscription) - - response = self.client_post("/json/billing/downgrade", {}) - self.assert_json_success(response) - stripe_customer = stripe_get_customer(stripe_customer.id) - # Check that positive balance was forgiven - self.assertEqual(stripe_customer.account_balance, 0) - self.assertIsNone(extract_current_subscription(stripe_customer)) - stripe_subscription = stripe.Subscription.retrieve(stripe_subscription.id) - self.assertEqual(stripe_subscription.status, "canceled") - @mock_stripe() def test_replace_payment_source(self, *mocks: Mock) -> None: user = self.example_user("hamlet") @@ -856,6 +759,7 @@ class RequiresBillingAccessTest(ZulipTestCase): params = [ ("/json/billing/sources/change", "do_replace_payment_source", {'stripe_token': ujson.dumps('token')}), + # TODO: second argument should be something like "process_downgrade" ("/json/billing/downgrade", "process_downgrade", {}), ] # type: List[Tuple[str, str, Dict[str, Any]]] diff --git a/corporate/views.py b/corporate/views.py index 382a4651a1..5d8bf90d7b 100644 --- a/corporate/views.py +++ b/corporate/views.py @@ -194,7 +194,7 @@ def billing_home(request: HttpRequest) -> HttpResponse: return render(request, 'corporate/billing.html', context=context) @require_billing_access -def downgrade(request: HttpRequest, user: UserProfile) -> HttpResponse: +def downgrade(request: HttpRequest, user: UserProfile) -> HttpResponse: # nocoverage try: process_downgrade(user) except BillingError as e: