diff --git a/corporate/tests/test_remote_billing.py b/corporate/tests/test_remote_billing.py index 8e232eb77e..6fcf5b58ec 100644 --- a/corporate/tests/test_remote_billing.py +++ b/corporate/tests/test_remote_billing.py @@ -23,7 +23,6 @@ from corporate.models import ( get_customer_by_remote_server, ) from corporate.views.remote_billing_page import generate_confirmation_link_for_server_deactivation -from zerver.actions.realm_settings import do_deactivate_realm from zerver.lib.exceptions import RemoteRealmServerMismatchError from zerver.lib.rate_limiter import RateLimitedIPAddr from zerver.lib.remote_server import send_server_data_to_push_bouncer @@ -662,98 +661,6 @@ class RemoteBillingAuthenticationTest(RemoteRealmBillingTestCase): ], ) - @responses.activate - def test_transfer_legacy_plan_from_server_to_all_realms(self) -> None: - self.login("desdemona") - desdemona = self.example_user("desdemona") - - # Assert current server is not on any plan. - self.assertIsNone(get_customer_by_remote_server(self.server)) - - start_date = timezone_now() - end_date = add_months(timezone_now(), 10) - - # Migrate server to legacy to plan. - server_billing_session = RemoteServerBillingSession(self.server) - server_billing_session.migrate_customer_to_legacy_plan(start_date, end_date) - - server_customer = server_billing_session.get_customer() - assert server_customer is not None - server_plan = get_current_plan_by_customer(server_customer) - assert server_plan is not None - self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY) - self.assertEqual(server_plan.tier, CustomerPlan.TIER_SELF_HOSTED_LEGACY) - self.assertEqual(server_plan.status, CustomerPlan.ACTIVE) - - # There are four test realms on this server: - # , , , - self.assert_length(Realm.objects.all(), 4) - - # Make lear deactivated, to have verification for that case. - do_deactivate_realm(Realm.objects.get(string_id="lear"), acting_user=None) - - # Delete any existing remote realms. - RemoteRealm.objects.all().delete() - - # First, set a sponsorship as pending. - # TODO: Ideally, we'd submit a proper sponsorship request. - server_customer.sponsorship_pending = True - server_customer.save() - - # Send server data to push bouncer. - self.add_mock_response() - send_server_data_to_push_bouncer(consider_usage_statistics=False) - - # Login to plan management. - result = self.execute_remote_billing_authentication_flow( - desdemona, return_from_auth_url=True - ) - self.assertEqual(result.status_code, 200) - self.assert_in_response("Plan management not available", result) - - # RemoteRealm objects should be created for all realms on the server. - self.assert_length(RemoteRealm.objects.all(), 4) - - # Server's plan should not have been migrated yet. - self.server.refresh_from_db() - self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY) - - # Now clear sponsorship_pending. - # TODO: Ideally, this would approve the sponsorship. - server_customer.sponsorship_pending = False - server_customer.save() - - # Login to plan management. Performs customer migration from server to realms. - result = self.execute_remote_billing_authentication_flow( - desdemona, return_from_auth_url=False - ) - self.assertEqual(result.status_code, 302) - - # Server plan status was reset - self.server.refresh_from_db() - self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED) - # Check no CustomerPlan exists for server. - self.assertIsNone(get_current_plan_by_customer(server_customer)) - - # Check legacy CustomerPlan exists for all realms except bot realm. - no_customer_plan_realms = set() - for remote_realm in RemoteRealm.objects.all(): - if remote_realm.is_system_bot_realm or remote_realm.realm_deactivated: - self.assertIsNone(get_customer_by_remote_realm(remote_realm)) - no_customer_plan_realms.add(remote_realm.host.split(".")[0]) - continue - - customer = get_customer_by_remote_realm(remote_realm) - assert customer is not None - plan = get_current_plan_by_customer(customer) - assert plan is not None - self.assertEqual(remote_realm.plan_type, RemoteRealm.PLAN_TYPE_SELF_MANAGED_LEGACY) - self.assertEqual(plan.tier, CustomerPlan.TIER_SELF_HOSTED_LEGACY) - self.assertEqual(plan.status, CustomerPlan.ACTIVE) - self.assertEqual(plan.billing_cycle_anchor, start_date) - self.assertEqual(plan.end_date, end_date) - self.assertEqual(no_customer_plan_realms, {"zulipinternal", "lear"}) - @responses.activate def test_transfer_legacy_plan_scheduled_for_upgrade_from_server_to_realm( self, diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index e0650f1b44..f913c76be7 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -91,6 +91,7 @@ from corporate.models import ( get_current_plan_by_customer, get_current_plan_by_realm, get_customer_by_realm, + get_customer_by_remote_realm, ) from corporate.tests.test_remote_billing import RemoteRealmBillingTestCase, RemoteServerTestCase from corporate.views.remote_billing_page import generate_confirmation_link_for_server_deactivation @@ -6699,33 +6700,27 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase): @responses.activate @mock_stripe() def test_schedule_legacy_plan_upgrade_to_fixed_price_plan(self, *mocks: Mock) -> None: - self.login("hamlet") hamlet = self.example_user("hamlet") - remote_server = RemoteZulipServer.objects.get(hostname="demo.example.com") - server_billing_session = RemoteServerBillingSession(remote_server=remote_server) + remote_realm = RemoteRealm.objects.get(uuid=hamlet.realm.uuid) + remote_realm_billing_session = RemoteRealmBillingSession(remote_realm=remote_realm) - # Migrate server to legacy plan. + # Migrate realm to legacy plan. with time_machine.travel(self.now, tick=False): start_date = timezone_now() end_date = add_months(start_date, months=3) - server_billing_session.migrate_customer_to_legacy_plan(start_date, end_date) + remote_realm_billing_session.migrate_customer_to_legacy_plan(start_date, end_date) self.add_mock_response() with time_machine.travel(self.now, tick=False): send_server_data_to_push_bouncer(consider_usage_statistics=False) - # Login. Performs customer migration from server to realms. - self.execute_remote_billing_authentication_flow(hamlet) - self.remote_realm.refresh_from_db() - customer = Customer.objects.get(remote_realm=self.remote_realm) legacy_plan = get_current_plan_by_customer(customer) assert legacy_plan is not None self.assertEqual(legacy_plan.tier, CustomerPlan.TIER_SELF_HOSTED_LEGACY) self.assertEqual(legacy_plan.next_invoice_date, end_date) - self.logout() self.login("iago") # Schedule a fixed-price business plan at current plan end_date. @@ -6759,7 +6754,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase): self.logout() self.login("hamlet") self.execute_remote_billing_authentication_flow( - hamlet, expect_tos=False, confirm_tos=False, first_time_login=False + hamlet, expect_tos=False, confirm_tos=True, first_time_login=True ) # Schedule upgrade to business plan @@ -7050,6 +7045,14 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase): self.assertEqual(server_customer_plan.status, CustomerPlan.ACTIVE) self.assertEqual(remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY) + # The plan gets migrated if there's only a single human realm. + Realm.objects.exclude(string_id__in=["zulip", "zulipinternal"]).delete() + + # First, set a sponsorship as pending. + # TODO: Ideally, we'd submit a proper sponsorship request. + server_customer.sponsorship_pending = True + server_customer.save() + # Upload data. with time_machine.travel(self.now, tick=False): self.add_mock_response() @@ -7059,27 +7062,42 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase): hamlet = self.example_user("hamlet") billing_base_url = self.billing_session.billing_base_url - # Login. Performs customer migration from server to realms. + # Login. The server has a pending sponsorship, in which case migrating + # can't be done, as that'd be a fairly confusing process. + result = self.execute_remote_billing_authentication_flow(hamlet, return_from_auth_url=True) + + self.assertEqual(result.status_code, 200) + self.assert_in_response("Plan management not available", result) + # Server's plan should not have been migrated yet. + self.server.refresh_from_db() + self.assertEqual(self.server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY) + + # Now clear the pending sponsorship state, which will allow login + # and migration to proceed. + # TODO: Ideally, this would approve the sponsorship and then be testing + # the migration of the Community plan. + server_customer.sponsorship_pending = False + server_customer.save() + + # Login. Performs customer migration from server to realm. result = self.execute_remote_billing_authentication_flow(hamlet) + self.assertEqual(result.status_code, 302) self.assertEqual(result["Location"], f"{billing_base_url}/plans/") remote_server.refresh_from_db() - server_customer_plan.refresh_from_db() - self.assertEqual(server_customer_plan.status, CustomerPlan.ENDED) + remote_realm = RemoteRealm.objects.get(uuid=hamlet.realm.uuid) + # The customer object was moved, together with the plan, from server to realm. + customer = get_customer_by_remote_realm(remote_realm) + assert customer is not None + self.assertEqual(server_customer, customer) self.assertEqual(remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_MANAGED) + self.assertEqual(remote_realm.plan_type, RemoteRealm.PLAN_TYPE_SELF_MANAGED_LEGACY) - remote_realms = RemoteRealm.objects.filter( - server=remote_server, - plan_type=RemoteRealm.PLAN_TYPE_SELF_MANAGED_LEGACY, - is_system_bot_realm=False, - ) - for remote_realm in remote_realms: - customer = Customer.objects.get(remote_realm=remote_realm) - customer_plan = get_current_plan_by_customer(customer) - assert customer_plan is not None - self.assertEqual(customer_plan.tier, CustomerPlan.TIER_SELF_HOSTED_LEGACY) - self.assertEqual(customer_plan.status, CustomerPlan.ACTIVE) + customer_plan = get_current_plan_by_customer(customer) + assert customer_plan is not None + self.assertEqual(customer_plan.tier, CustomerPlan.TIER_SELF_HOSTED_LEGACY) + self.assertEqual(customer_plan.status, CustomerPlan.ACTIVE) # upgrade to business plan with time_machine.travel(self.now, tick=False): @@ -7090,17 +7108,15 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase): with time_machine.travel(self.now, tick=False): stripe_customer = self.add_card_and_upgrade() - zulip_realm_customer = Customer.objects.get(stripe_customer_id=stripe_customer.id) - zulip_realm_plan = CustomerPlan.objects.get( - customer=zulip_realm_customer, status=CustomerPlan.ACTIVE - ) - self.assertEqual(zulip_realm_plan.tier, CustomerPlan.TIER_SELF_HOSTED_BUSINESS) + self.assertEqual(customer, Customer.objects.get(stripe_customer_id=stripe_customer.id)) + business_plan = CustomerPlan.objects.get(customer=customer, status=CustomerPlan.ACTIVE) + self.assertEqual(business_plan.tier, CustomerPlan.TIER_SELF_HOSTED_BUSINESS) realm_user_count = UserProfile.objects.filter( realm=hamlet.realm, is_bot=False, is_active=True ).count() licenses = max( - realm_user_count, self.billing_session.min_licenses_for_plan(zulip_realm_plan.tier) + realm_user_count, self.billing_session.min_licenses_for_plan(business_plan.tier) ) with time_machine.travel(self.now + timedelta(days=1), tick=False): response = self.client_get(f"{billing_base_url}/billing/", subdomain="selfhosting") @@ -7130,11 +7146,11 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase): "/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}, ) - expected_log = f"INFO:corporate.stripe:Change plan status: Customer.id: {zulip_realm_customer.id}, CustomerPlan.id: {zulip_realm_plan.id}, status: {CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}" + expected_log = f"INFO:corporate.stripe:Change plan status: Customer.id: {customer.id}, CustomerPlan.id: {business_plan.id}, status: {CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}" self.assertEqual(m.output[0], expected_log) self.assert_json_success(response) - zulip_realm_plan.refresh_from_db() - self.assertEqual(zulip_realm_plan.licenses_at_next_renewal(), None) + business_plan.refresh_from_db() + self.assertEqual(business_plan.licenses_at_next_renewal(), None) @responses.activate @mock_stripe() @@ -7277,14 +7293,16 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase): @responses.activate @mock_stripe() def test_invoice_scheduled_upgrade_realm_legacy_plan(self, *mocks: Mock) -> None: - remote_server = RemoteZulipServer.objects.get(hostname="demo.example.com") - server_billing_session = RemoteServerBillingSession(remote_server=remote_server) + hamlet = self.example_user("hamlet") - # Migrate server to legacy plan. + remote_realm = RemoteRealm.objects.get(uuid=hamlet.realm.uuid) + remote_realm_billing_session = RemoteRealmBillingSession(remote_realm=remote_realm) + + # Migrate realm to legacy plan. with time_machine.travel(self.now, tick=False): start_date = timezone_now() end_date = add_months(start_date, months=3) - server_billing_session.migrate_customer_to_legacy_plan(start_date, end_date) + remote_realm_billing_session.migrate_customer_to_legacy_plan(start_date, end_date) # Upload data. self.add_mock_response() @@ -7292,11 +7310,9 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase): send_server_data_to_push_bouncer(consider_usage_statistics=False) self.login("hamlet") - hamlet = self.example_user("hamlet") - # Login. Performs customer migration from server to realms. + # Login. self.execute_remote_billing_authentication_flow(hamlet) - remote_server.refresh_from_db() # Schedule upgrade to business plan with time_machine.travel(self.now, tick=False): diff --git a/corporate/views/remote_billing_page.py b/corporate/views/remote_billing_page.py index 7ca7ffe25a..1ddcc9aee4 100644 --- a/corporate/views/remote_billing_page.py +++ b/corporate/views/remote_billing_page.py @@ -65,7 +65,7 @@ from zilencer.models import ( RemoteZulipServer, get_remote_server_by_uuid, ) -from zilencer.views import handle_customer_migration_from_server_to_realms +from zilencer.views import handle_customer_migration_from_server_to_realm billing_logger = logging.getLogger("corporate.stripe") @@ -200,7 +200,7 @@ def remote_realm_billing_finalize_login( raise AssertionError try: - handle_customer_migration_from_server_to_realms(server=remote_server) + handle_customer_migration_from_server_to_realm(server=remote_server) except JsonableError: # JsonableError should be propagated up, as they are meant to convey # a json error response to be returned. diff --git a/zilencer/views.py b/zilencer/views.py index 23eb6ca30a..517004c9b6 100644 --- a/zilencer/views.py +++ b/zilencer/views.py @@ -935,7 +935,7 @@ def get_human_user_realm_uuids( @transaction.atomic -def handle_customer_migration_from_server_to_realms( +def handle_customer_migration_from_server_to_realm( server: RemoteZulipServer, ) -> None: server_billing_session = RemoteServerBillingSession(server) @@ -967,42 +967,7 @@ def handle_customer_migration_from_server_to_realms( event_time = timezone_now() remote_realm_audit_logs = [] - if ( - server_plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY - and server_plan.status == CustomerPlan.ACTIVE - ): - assert server.plan_type == RemoteZulipServer.PLAN_TYPE_SELF_MANAGED_LEGACY - assert server_plan.end_date is not None - remote_realms = RemoteRealm.objects.filter( - uuid__in=realm_uuids, server=server, plan_type=RemoteRealm.PLAN_TYPE_SELF_MANAGED - ) - - # Verify that all the realms are on self hosted plan. - assert remote_realms.count() == len(realm_uuids) - - # End existing plan for server. - server_plan.status = CustomerPlan.ENDED - server_plan.save(update_fields=["status"]) - - server.plan_type = RemoteZulipServer.PLAN_TYPE_SELF_MANAGED - server.save(update_fields=["plan_type"]) - - # Create new legacy plan for each remote realm. - for remote_realm in remote_realms: - RemoteRealmBillingSession(remote_realm).migrate_customer_to_legacy_plan( - server_plan.billing_cycle_anchor, server_plan.end_date - ) - remote_realm_audit_logs.append( - RemoteRealmAuditLog( - server=server, - remote_realm=remote_realm, - event_type=RemoteRealmAuditLog.REMOTE_PLAN_TRANSFERRED_SERVER_TO_REALM, - event_time=event_time, - # No extra_data since there was no real change in any RemoteRealm attribute. - ) - ) - - elif len(realm_uuids) == 1: + if len(realm_uuids) == 1: # Here, we have exactly one non-system-bot realm, and some # sort of plan on the server; move it to the realm. remote_realm = RemoteRealm.objects.get(uuid=realm_uuids[0], server=server)