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:
Lauryn Menard 2023-12-21 20:53:54 +01:00 committed by Tim Abbott
parent 8f795e22e8
commit ffd708ecaf
5 changed files with 129 additions and 7 deletions

View File

@ -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):

View File

@ -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

View File

@ -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 />

View File

@ -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 %}

View File

@ -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>