mirror of https://github.com/zulip/zulip.git
support: Display next plan data on remote support view.
Currently, this will only be the case for legacy self-managed plans that have scheduled a switch to either the Basic or Business plan.
This commit is contained in:
parent
8f795e22e8
commit
ffd708ecaf
|
@ -8,7 +8,7 @@ import time_machine
|
|||
from django.utils.timezone import now as timezone_now
|
||||
from typing_extensions import override
|
||||
|
||||
from corporate.lib.stripe import RealmBillingSession, add_months
|
||||
from corporate.lib.stripe import RealmBillingSession, RemoteRealmBillingSession, add_months
|
||||
from corporate.models import (
|
||||
Customer,
|
||||
CustomerPlan,
|
||||
|
@ -54,6 +54,41 @@ class TestRemoteServerSupportEndpoint(ZulipTestCase):
|
|||
requested_plan=plan,
|
||||
)
|
||||
|
||||
def add_legacy_plan_and_upgrade(name: str) -> None:
|
||||
legacy_anchor = datetime(2050, 1, 1, tzinfo=timezone.utc)
|
||||
next_plan_anchor = datetime(2050, 2, 1, tzinfo=timezone.utc)
|
||||
billed_licenses = 10
|
||||
remote_realm = RemoteRealm.objects.get(name=name)
|
||||
billing_session = RemoteRealmBillingSession(remote_realm)
|
||||
|
||||
billing_session.migrate_customer_to_legacy_plan(legacy_anchor, next_plan_anchor)
|
||||
customer = billing_session.get_customer()
|
||||
assert customer is not None
|
||||
legacy_plan = billing_session.get_remote_server_legacy_plan(customer)
|
||||
assert legacy_plan is not None
|
||||
assert legacy_plan.end_date is not None
|
||||
last_ledger_entry = (
|
||||
LicenseLedger.objects.filter(plan=legacy_plan).order_by("-id").first()
|
||||
)
|
||||
assert last_ledger_entry is not None
|
||||
last_ledger_entry.licenses_at_next_renewal = billed_licenses
|
||||
last_ledger_entry.save(update_fields=["licenses_at_next_renewal"])
|
||||
legacy_plan.status = CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END
|
||||
legacy_plan.save(update_fields=["status"])
|
||||
plan_params = {
|
||||
"automanage_licenses": True,
|
||||
"charge_automatically": False,
|
||||
"price_per_license": 100,
|
||||
"discount": customer.default_discount,
|
||||
"billing_cycle_anchor": next_plan_anchor,
|
||||
"billing_schedule": CustomerPlan.BILLING_SCHEDULE_MONTHLY,
|
||||
"tier": CustomerPlan.TIER_SELF_HOSTED_BASIC,
|
||||
"status": CustomerPlan.NEVER_STARTED,
|
||||
}
|
||||
CustomerPlan.objects.create(
|
||||
customer=customer, next_invoice_date=next_plan_anchor, **plan_params
|
||||
)
|
||||
|
||||
super().setUp()
|
||||
|
||||
# Set up some initial example data.
|
||||
|
@ -93,6 +128,9 @@ class TestRemoteServerSupportEndpoint(ZulipTestCase):
|
|||
plan=SponsoredPlanTypes.COMMUNITY.value,
|
||||
)
|
||||
|
||||
# Add expected legacy customer and plan data
|
||||
add_legacy_plan_and_upgrade(name="realm-name-3")
|
||||
|
||||
def test_search(self) -> None:
|
||||
def assert_server_details_in_response(
|
||||
html_response: "TestHttpResponse", hostname: str
|
||||
|
@ -170,6 +208,25 @@ class TestRemoteServerSupportEndpoint(ZulipTestCase):
|
|||
result,
|
||||
)
|
||||
|
||||
def check_legacy_and_next_plan(result: "TestHttpResponse") -> None:
|
||||
self.assert_in_success_response(
|
||||
[
|
||||
"<h4>📅 Current plan information:</h4>",
|
||||
"<b>Plan name</b>: Self-managed (legacy plan)<br />",
|
||||
"<b>Status</b>: New plan scheduled<br />",
|
||||
"<b>End date</b>: 01 February 2050<br />",
|
||||
"<h4>⏱️ Next plan information:</h4>",
|
||||
"<b>Plan name</b>: Zulip Basic<br />",
|
||||
"<b>Status</b>: Never started<br />",
|
||||
"<b>Start date</b>: 01 February 2050<br />",
|
||||
"<b>Billing schedule</b>: Monthly<br />",
|
||||
"<b>Price per license</b>: $1.00<br />",
|
||||
"<b>Estimated billed licenses</b>: 10<br />",
|
||||
"<b>Estimated annual revenue</b>: $120.00<br />",
|
||||
],
|
||||
result,
|
||||
)
|
||||
|
||||
self.login("cordelia")
|
||||
|
||||
result = self.client_get("/activity/remote/support")
|
||||
|
@ -228,6 +285,7 @@ class TestRemoteServerSupportEndpoint(ZulipTestCase):
|
|||
assert_server_details_in_response(result, f"zulip-{server}.example.com")
|
||||
assert_realm_details_in_response(result, f"realm-name-{server}", f"realm-host-{server}")
|
||||
check_no_sponsorship_request(result)
|
||||
check_legacy_and_next_plan(result)
|
||||
|
||||
|
||||
class TestSupportEndpoint(ZulipTestCase):
|
||||
|
|
|
@ -49,12 +49,14 @@ class SponsorshipData:
|
|||
class PlanData:
|
||||
customer: Optional["Customer"] = None
|
||||
current_plan: Optional["CustomerPlan"] = None
|
||||
next_plan: Optional["CustomerPlan"] = None
|
||||
licenses: Optional[int] = None
|
||||
licenses_used: Optional[int] = None
|
||||
is_legacy_plan: bool = False
|
||||
has_fixed_price: bool = False
|
||||
warning: Optional[str] = None
|
||||
annual_recurring_revenue: Optional[int] = None
|
||||
estimated_next_plan_revenue: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -115,6 +117,13 @@ def get_customer_sponsorship_data(customer: Customer) -> SponsorshipData:
|
|||
)
|
||||
|
||||
|
||||
def get_annual_invoice_count(billing_schedule: int) -> int:
|
||||
if billing_schedule == CustomerPlan.BILLING_SCHEDULE_MONTHLY:
|
||||
return 12
|
||||
else:
|
||||
return 1
|
||||
|
||||
|
||||
def get_current_plan_data_for_support_view(billing_session: BillingSession) -> PlanData:
|
||||
customer = billing_session.get_customer()
|
||||
plan = None
|
||||
|
@ -136,18 +145,36 @@ def get_current_plan_data_for_support_view(billing_session: BillingSession) -> P
|
|||
plan_data.licenses_used = billing_session.current_count_for_billed_licenses()
|
||||
except MissingDataError: # nocoverage
|
||||
plan_data.warning = "Recent data missing: No information for used licenses"
|
||||
|
||||
assert plan_data.current_plan is not None # for mypy
|
||||
|
||||
plan_data.next_plan = billing_session.get_next_plan(plan_data.current_plan)
|
||||
|
||||
if plan_data.next_plan is not None:
|
||||
if plan_data.next_plan.fixed_price is not None: # nocoverage
|
||||
plan_data.estimated_next_plan_revenue = plan_data.next_plan.fixed_price
|
||||
elif plan_data.current_plan.licenses_at_next_renewal() is not None:
|
||||
next_plan_licenses = plan_data.current_plan.licenses_at_next_renewal()
|
||||
assert next_plan_licenses is not None
|
||||
assert plan_data.next_plan.price_per_license is not None
|
||||
invoice_count = get_annual_invoice_count(plan_data.next_plan.billing_schedule)
|
||||
plan_data.estimated_next_plan_revenue = (
|
||||
plan_data.next_plan.price_per_license * next_plan_licenses * invoice_count
|
||||
)
|
||||
else:
|
||||
plan_data.estimated_next_plan_revenue = 0 # nocoverage
|
||||
|
||||
plan_data.is_legacy_plan = (
|
||||
plan_data.current_plan.tier == CustomerPlan.TIER_SELF_HOSTED_LEGACY
|
||||
)
|
||||
plan_data.has_fixed_price = plan_data.current_plan.fixed_price is not None
|
||||
plan_revenue = billing_session.get_customer_plan_renewal_amount(
|
||||
plan_data.current_plan, timezone_now(), last_ledger_entry
|
||||
annual_invoice_count = get_annual_invoice_count(plan_data.current_plan.billing_schedule)
|
||||
plan_data.annual_recurring_revenue = (
|
||||
billing_session.get_customer_plan_renewal_amount(
|
||||
plan_data.current_plan, timezone_now(), last_ledger_entry
|
||||
)
|
||||
* annual_invoice_count
|
||||
)
|
||||
if plan_data.current_plan.billing_schedule == CustomerPlan.BILLING_SCHEDULE_MONTHLY:
|
||||
plan_data.annual_recurring_revenue = plan_revenue * 12
|
||||
else:
|
||||
plan_data.annual_recurring_revenue = plan_revenue
|
||||
|
||||
return plan_data
|
||||
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<h4>⏱️ Next plan information:</h4>
|
||||
<b>Plan name</b>: {{ plan_data.next_plan.name }}<br />
|
||||
<b>Status</b>: {{ plan_data.next_plan.get_plan_status_as_text() }}<br />
|
||||
<b>Start date</b>: {{ plan_data.next_plan.billing_cycle_anchor.strftime('%d %B %Y') }}<br />
|
||||
<b>Billing schedule</b>: {% if plan_data.next_plan.billing_schedule == plan_data.next_plan.BILLING_SCHEDULE_ANNUAL %}Annual{% else %}Monthly{% endif %}<br />
|
||||
{% if plan_data.next_plan.discount %}
|
||||
<b>Discount</b>: {{ format_discount(plan_data.next_plan.discount) }}%<br />
|
||||
{% endif %}
|
||||
{% if plan_data.next_plan.price_per_license %}
|
||||
<b>Price per license</b>: ${{ dollar_amount(plan_data.next_plan.price_per_license) }}<br />
|
||||
<b>Estimated billed licenses</b>: {{ plan_data.current_plan.licenses_at_next_renewal() }}<br />
|
||||
{% elif plan_data.next_plan.fixed_price %}
|
||||
<b>Plan has a fixed price.</b>
|
||||
{% endif %}
|
||||
<b>Estimated annual revenue</b>: ${{ dollar_amount(plan_data.estimated_next_plan_revenue) }}<br />
|
|
@ -42,3 +42,14 @@
|
|||
{% include 'analytics/current_plan_forms_support.html' %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if support_data[remote_realm.id].plan_data.next_plan %}
|
||||
<div class="remote-realm-information">
|
||||
{% with %}
|
||||
{% set plan_data = support_data[remote_realm.id].plan_data %}
|
||||
{% set format_discount = format_discount %}
|
||||
{% set dollar_amount = dollar_amount %}
|
||||
{% include 'analytics/next_plan_details.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
|
|
@ -86,6 +86,17 @@
|
|||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if remote_servers_support_data[remote_server.id].plan_data.next_plan %}
|
||||
<div class="remote-server-information">
|
||||
{% with %}
|
||||
{% set plan_data = remote_servers_support_data[remote_server.id].plan_data %}
|
||||
{% set format_discount = format_discount %}
|
||||
{% set dollar_amount = dollar_amount %}
|
||||
{% include 'analytics/next_plan_details.html' %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for remote_realm in remote_realms[remote_server.id] %}
|
||||
<hr />
|
||||
<div>
|
||||
|
|
Loading…
Reference in New Issue