mirror of https://github.com/zulip/zulip.git
post_analytics: Migrate plan from server to realm after upgrade.
This commit is contained in:
parent
64517a7ad3
commit
23d712391e
|
@ -1113,8 +1113,8 @@ class BillingSession(ABC):
|
||||||
def ensure_current_plan_is_upgradable(
|
def ensure_current_plan_is_upgradable(
|
||||||
self, customer: Customer, new_plan_tier: int
|
self, customer: Customer, new_plan_tier: int
|
||||||
) -> None: # nocoverage
|
) -> None: # nocoverage
|
||||||
# Upgrade for customers with an existing plan is only supported for remote servers right now.
|
# Upgrade for customers with an existing plan is only supported for remote realm / server right now.
|
||||||
if not hasattr(self, "remote_server"):
|
if isinstance(self, RealmBillingSession):
|
||||||
ensure_customer_does_not_have_active_plan(customer)
|
ensure_customer_does_not_have_active_plan(customer)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -3032,6 +3032,7 @@ class RemoteRealmBillingSession(BillingSession): # nocoverage
|
||||||
raise AssertionError("Unexpected tier")
|
raise AssertionError("Unexpected tier")
|
||||||
|
|
||||||
# TODO: Audit logging and set usage limits.
|
# TODO: Audit logging and set usage limits.
|
||||||
|
# TODO: Set the usage limit in handle_customer_migration_from_server_to_realms.
|
||||||
|
|
||||||
self.remote_realm.plan_type = plan_type
|
self.remote_realm.plan_type = plan_type
|
||||||
self.remote_realm.save(update_fields=["plan_type"])
|
self.remote_realm.save(update_fields=["plan_type"])
|
||||||
|
@ -3117,6 +3118,7 @@ class RemoteRealmBillingSession(BillingSession): # nocoverage
|
||||||
self, current_plan_tier: int, new_plan_tier: int
|
self, current_plan_tier: int, new_plan_tier: int
|
||||||
) -> PlanTierChangeType:
|
) -> PlanTierChangeType:
|
||||||
valid_plan_tiers = [
|
valid_plan_tiers = [
|
||||||
|
CustomerPlan.TIER_SELF_HOSTED_LEGACY,
|
||||||
CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
||||||
CustomerPlan.TIER_SELF_HOSTED_PLUS,
|
CustomerPlan.TIER_SELF_HOSTED_PLUS,
|
||||||
]
|
]
|
||||||
|
@ -3131,6 +3133,11 @@ class RemoteRealmBillingSession(BillingSession): # nocoverage
|
||||||
and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_PLUS
|
and new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_PLUS
|
||||||
):
|
):
|
||||||
return PlanTierChangeType.UPGRADE
|
return PlanTierChangeType.UPGRADE
|
||||||
|
elif current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY and new_plan_tier in (
|
||||||
|
CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
||||||
|
CustomerPlan.TIER_SELF_HOSTED_PLUS,
|
||||||
|
):
|
||||||
|
return PlanTierChangeType.UPGRADE
|
||||||
else:
|
else:
|
||||||
assert current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_PLUS
|
assert current_plan_tier == CustomerPlan.TIER_SELF_HOSTED_PLUS
|
||||||
assert new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS
|
assert new_plan_tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS
|
||||||
|
|
|
@ -117,12 +117,16 @@ def remote_realm_billing_page(
|
||||||
# If the realm is on a paid plan, show the sponsorship pending message
|
# If the realm is on a paid plan, show the sponsorship pending message
|
||||||
context["sponsorship_pending"] = True
|
context["sponsorship_pending"] = True
|
||||||
|
|
||||||
if billing_session.remote_realm.plan_type == RemoteRealm.PLAN_TYPE_SELF_HOSTED:
|
if (
|
||||||
|
customer is None
|
||||||
|
or get_current_plan_by_customer(customer) is None
|
||||||
|
or (
|
||||||
|
billing_session.get_legacy_remote_server_next_plan_name(customer) is None
|
||||||
|
and billing_session.remote_realm.plan_type == RemoteRealm.PLAN_TYPE_SELF_HOSTED
|
||||||
|
)
|
||||||
|
):
|
||||||
return HttpResponseRedirect(reverse("remote_realm_plans_page", args=(realm_uuid,)))
|
return HttpResponseRedirect(reverse("remote_realm_plans_page", args=(realm_uuid,)))
|
||||||
|
|
||||||
if customer is None or get_current_plan_by_customer(customer) is None:
|
|
||||||
return HttpResponseRedirect(reverse("remote_realm_upgrade_page", args=(realm_uuid,)))
|
|
||||||
|
|
||||||
main_context = billing_session.get_billing_page_context()
|
main_context = billing_session.get_billing_page_context()
|
||||||
if main_context:
|
if main_context:
|
||||||
context.update(main_context)
|
context.update(main_context)
|
||||||
|
|
|
@ -149,7 +149,21 @@ def remote_realm_plans_page(
|
||||||
if context.customer_plan is None:
|
if context.customer_plan is None:
|
||||||
context.on_free_tier = not context.is_sponsored
|
context.on_free_tier = not context.is_sponsored
|
||||||
else:
|
else:
|
||||||
|
context.on_free_tier = context.customer_plan.tier in (
|
||||||
|
CustomerPlan.TIER_SELF_HOSTED_LEGACY,
|
||||||
|
CustomerPlan.TIER_SELF_HOSTED_BASE,
|
||||||
|
)
|
||||||
context.on_free_trial = is_customer_on_free_trial(context.customer_plan)
|
context.on_free_trial = is_customer_on_free_trial(context.customer_plan)
|
||||||
|
context.is_legacy_server_with_scheduled_upgrade = (
|
||||||
|
context.customer_plan.status == CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END
|
||||||
|
)
|
||||||
|
if context.is_legacy_server_with_scheduled_upgrade:
|
||||||
|
assert context.customer_plan.end_date is not None
|
||||||
|
context.legacy_server_new_plan = CustomerPlan.objects.get(
|
||||||
|
customer=customer,
|
||||||
|
billing_cycle_anchor=context.customer_plan.end_date,
|
||||||
|
status=CustomerPlan.NEVER_STARTED,
|
||||||
|
)
|
||||||
|
|
||||||
context.is_new_customer = (
|
context.is_new_customer = (
|
||||||
not context.on_free_tier and context.customer_plan is None and not context.is_sponsored
|
not context.on_free_tier and context.customer_plan is None and not context.is_sponsored
|
||||||
|
|
|
@ -95,6 +95,7 @@ def remote_realm_upgrade(
|
||||||
default=None, str_validator=check_string_in(VALID_LICENSE_MANAGEMENT_VALUES)
|
default=None, str_validator=check_string_in(VALID_LICENSE_MANAGEMENT_VALUES)
|
||||||
),
|
),
|
||||||
licenses: Optional[int] = REQ(json_validator=check_int, default=None),
|
licenses: Optional[int] = REQ(json_validator=check_int, default=None),
|
||||||
|
remote_server_plan_start_date: Optional[str] = REQ(default=None),
|
||||||
) -> HttpResponse: # nocoverage
|
) -> HttpResponse: # nocoverage
|
||||||
try:
|
try:
|
||||||
upgrade_request = UpgradeRequest(
|
upgrade_request = UpgradeRequest(
|
||||||
|
@ -106,7 +107,7 @@ def remote_realm_upgrade(
|
||||||
licenses=licenses,
|
licenses=licenses,
|
||||||
# TODO: tier should be a passed parameter.
|
# TODO: tier should be a passed parameter.
|
||||||
tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
||||||
remote_server_plan_start_date=None,
|
remote_server_plan_start_date=remote_server_plan_start_date,
|
||||||
)
|
)
|
||||||
data = billing_session.do_upgrade(upgrade_request)
|
data = billing_session.do_upgrade(upgrade_request)
|
||||||
return json_success(request, data)
|
return json_success(request, data)
|
||||||
|
@ -212,10 +213,12 @@ def remote_realm_upgrade_page(
|
||||||
billing_session: RemoteRealmBillingSession,
|
billing_session: RemoteRealmBillingSession,
|
||||||
*,
|
*,
|
||||||
manual_license_management: Json[bool] = False,
|
manual_license_management: Json[bool] = False,
|
||||||
|
success_message: str = "",
|
||||||
) -> HttpResponse: # nocoverage
|
) -> HttpResponse: # nocoverage
|
||||||
initial_upgrade_request = InitialUpgradeRequest(
|
initial_upgrade_request = InitialUpgradeRequest(
|
||||||
manual_license_management=manual_license_management,
|
manual_license_management=manual_license_management,
|
||||||
tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
tier=CustomerPlan.TIER_SELF_HOSTED_BUSINESS,
|
||||||
|
success_message=success_message,
|
||||||
)
|
)
|
||||||
redirect_url, context = billing_session.get_initial_upgrade_context(initial_upgrade_request)
|
redirect_url, context = billing_session.get_initial_upgrade_context(initial_upgrade_request)
|
||||||
|
|
||||||
|
|
|
@ -4858,6 +4858,7 @@ class AbstractRealmAuditLog(models.Model):
|
||||||
# RemoteRealm model resulting from modified realm information sent to us
|
# RemoteRealm model resulting from modified realm information sent to us
|
||||||
# via send_analytics_to_push_bouncer.
|
# via send_analytics_to_push_bouncer.
|
||||||
REMOTE_REALM_VALUE_UPDATED = 20001
|
REMOTE_REALM_VALUE_UPDATED = 20001
|
||||||
|
REMOTE_PLAN_TRANSFERRED_SERVER_TO_REALM = 20002
|
||||||
|
|
||||||
event_type = models.PositiveSmallIntegerField()
|
event_type = models.PositiveSmallIntegerField()
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional, Type, TypeVar, Union
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import URLValidator, validate_email
|
from django.core.validators import URLValidator, validate_email
|
||||||
from django.db import IntegrityError, transaction
|
from django.db import IntegrityError, transaction
|
||||||
|
@ -23,7 +24,11 @@ from analytics.lib.counts import (
|
||||||
REMOTE_INSTALLATION_COUNT_STATS,
|
REMOTE_INSTALLATION_COUNT_STATS,
|
||||||
do_increment_logging_stat,
|
do_increment_logging_stat,
|
||||||
)
|
)
|
||||||
from corporate.lib.stripe import RemoteRealmBillingSession, do_deactivate_remote_server
|
from corporate.lib.stripe import (
|
||||||
|
RemoteRealmBillingSession,
|
||||||
|
RemoteServerBillingSession,
|
||||||
|
do_deactivate_remote_server,
|
||||||
|
)
|
||||||
from corporate.models import CustomerPlan, get_current_plan_by_customer
|
from corporate.models import CustomerPlan, get_current_plan_by_customer
|
||||||
from zerver.decorator import require_post
|
from zerver.decorator import require_post
|
||||||
from zerver.lib.exceptions import JsonableError, RemoteRealmServerMismatchError
|
from zerver.lib.exceptions import JsonableError, RemoteRealmServerMismatchError
|
||||||
|
@ -673,6 +678,101 @@ def update_remote_realm_data_for_server(
|
||||||
RemoteRealmAuditLog.objects.bulk_create(remote_realm_audit_logs)
|
RemoteRealmAuditLog.objects.bulk_create(remote_realm_audit_logs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_human_user_realm_uuids(realms: List[RealmDataForAnalytics]) -> List[UUID]: # nocoverage
|
||||||
|
billable_realm_uuids = []
|
||||||
|
for realm in realms:
|
||||||
|
# TODO: Remove the `zulipinternal` string_id check once no server is on 8.0-beta.
|
||||||
|
if (
|
||||||
|
realm.is_system_bot_realm
|
||||||
|
or realm.host.startswith("zulipinternal.")
|
||||||
|
or (settings.DEVELOPMENT and realm.host.startswith("analytics."))
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
billable_realm_uuids.append(realm.uuid)
|
||||||
|
|
||||||
|
return billable_realm_uuids
|
||||||
|
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def handle_customer_migration_from_server_to_realms(
|
||||||
|
server: RemoteZulipServer, realms: List[RealmDataForAnalytics]
|
||||||
|
) -> None: # nocoverage
|
||||||
|
server_billing_session = RemoteServerBillingSession(server)
|
||||||
|
server_customer = server_billing_session.get_customer()
|
||||||
|
if server_customer is None:
|
||||||
|
return
|
||||||
|
server_plan = get_current_plan_by_customer(server_customer)
|
||||||
|
realm_uuids = get_human_user_realm_uuids(realms)
|
||||||
|
if not realm_uuids:
|
||||||
|
return
|
||||||
|
|
||||||
|
event_time = timezone_now()
|
||||||
|
remote_realm_audit_logs = []
|
||||||
|
if (
|
||||||
|
server_plan is not None
|
||||||
|
and server_plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY
|
||||||
|
and server_plan.status == CustomerPlan.ACTIVE
|
||||||
|
):
|
||||||
|
assert server.plan_type == RemoteZulipServer.PLAN_TYPE_SELF_HOSTED
|
||||||
|
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_HOSTED
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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"])
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# We only do this migration if there is only one realm besides the system bot realm.
|
||||||
|
elif len(realm_uuids) == 1:
|
||||||
|
remote_realm = RemoteRealm.objects.get(
|
||||||
|
uuid=realm_uuids[0], plan_type=RemoteRealm.PLAN_TYPE_SELF_HOSTED
|
||||||
|
)
|
||||||
|
# Migrate customer from server to remote realm if there is only one realm.
|
||||||
|
server_customer.remote_realm = remote_realm
|
||||||
|
server_customer.remote_server = None
|
||||||
|
server_customer.save(update_fields=["remote_realm", "remote_server"])
|
||||||
|
# TODO: Set usage limits for remote realm and server.
|
||||||
|
# Might be better to call do_change_plan_type here.
|
||||||
|
remote_realm.plan_type = server.plan_type
|
||||||
|
remote_realm.save(update_fields=["plan_type"])
|
||||||
|
server.plan_type = RemoteZulipServer.PLAN_TYPE_SELF_HOSTED
|
||||||
|
server.save(update_fields=["plan_type"])
|
||||||
|
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,
|
||||||
|
extra_data={
|
||||||
|
"attr_name": "plan_type",
|
||||||
|
"old_value": RemoteRealm.PLAN_TYPE_SELF_HOSTED,
|
||||||
|
"new_value": remote_realm.plan_type,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
RemoteRealmAuditLog.objects.bulk_create(remote_realm_audit_logs)
|
||||||
|
|
||||||
|
|
||||||
@typed_endpoint
|
@typed_endpoint
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def remote_server_post_analytics(
|
def remote_server_post_analytics(
|
||||||
|
@ -725,6 +825,21 @@ def remote_server_post_analytics(
|
||||||
if remote_server_version_updated:
|
if remote_server_version_updated:
|
||||||
fix_remote_realm_foreign_keys(server, realms)
|
fix_remote_realm_foreign_keys(server, realms)
|
||||||
|
|
||||||
|
try:
|
||||||
|
handle_customer_migration_from_server_to_realms(server, realms)
|
||||||
|
except Exception: # nocoverage
|
||||||
|
logger.exception(
|
||||||
|
"%s: Failed to migrate customer from server (id: %s) to realms",
|
||||||
|
request.path,
|
||||||
|
server.id,
|
||||||
|
stack_info=True,
|
||||||
|
)
|
||||||
|
raise JsonableError(
|
||||||
|
_(
|
||||||
|
"Failed to migrate customer from server to realms. Please contact support for assistance."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
realm_id_to_remote_realm = build_realm_id_to_remote_realm_dict(server, realms)
|
realm_id_to_remote_realm = build_realm_id_to_remote_realm_dict(server, realms)
|
||||||
|
|
||||||
remote_realm_counts = [
|
remote_realm_counts = [
|
||||||
|
|
Loading…
Reference in New Issue