billing: Apply a flat discount for self hosted plans.

This commit is contained in:
Aman Agrawal 2023-12-20 06:24:21 +00:00 committed by Tim Abbott
parent 0337c726d3
commit e192aef23d
95 changed files with 439 additions and 55 deletions

View File

@ -588,6 +588,8 @@ class UpgradePageParams(TypedDict):
seat_count: int
billing_base_url: str
tier: int
flat_discount: int
flat_discounted_months: int
class UpgradePageSessionTypeSpecificContext(TypedDict):
@ -706,9 +708,11 @@ class BillingSession(ABC):
def get_data_for_stripe_payment_intent(
self,
customer: Customer,
price_per_license: int,
licenses: int,
plan_tier: int,
billing_schedule: int,
email: str,
) -> StripePaymentIntentData:
if hasattr(self, "support_session") and self.support_session: # nocoverage
@ -721,6 +725,12 @@ class BillingSession(ABC):
plan_name = CustomerPlan.name_from_tier(plan_tier)
description = f"Upgrade to {plan_name}, ${price_per_license/100} x {licenses}"
if customer.flat_discounted_months > 0:
num_months = 12 if billing_schedule == CustomerPlan.BILLING_SCHEDULE_ANNUAL else 1
flat_discounted_months = min(customer.flat_discounted_months, num_months)
amount -= customer.flat_discount * flat_discounted_months
description += f" - ${customer.flat_discount/100} x {flat_discounted_months}"
return StripePaymentIntentData(
amount=amount,
description=description,
@ -917,7 +927,12 @@ class BillingSession(ABC):
customer = self.get_customer()
assert customer is not None and customer.stripe_customer_id is not None
payment_intent_data = self.get_data_for_stripe_payment_intent(
price_per_license, licenses, metadata["plan_tier"], self.get_email()
customer,
price_per_license,
licenses,
metadata["plan_tier"],
metadata["billing_schedule"],
self.get_email(),
)
# Ensure customers have a default payment method set.
stripe_customer = stripe_get_customer(customer.stripe_customer_id)
@ -1021,14 +1036,18 @@ class BillingSession(ABC):
plan.save(update_fields=["discount", "price_per_license"])
def attach_discount_to_customer(self, new_discount: Decimal) -> str:
# Remove flat discount if giving customer a percentage discount.
customer = self.get_customer()
old_discount = None
if customer is not None:
old_discount = customer.default_discount
customer.default_discount = new_discount
customer.save(update_fields=["default_discount"])
customer.flat_discounted_months = 0
customer.save(update_fields=["default_discount", "flat_discounted_months"])
else:
customer = self.update_or_create_customer(defaults={"default_discount": new_discount})
customer = self.update_or_create_customer(
defaults={"default_discount": new_discount, "flat_discounted_months": 0}
)
plan = get_current_plan_by_customer(customer)
if plan is not None:
self.apply_discount_to_plan(plan, new_discount)
@ -1305,6 +1324,21 @@ class BillingSession(ABC):
unit_amount=price_per_license,
)
if customer.flat_discounted_months > 0:
num_months = 12 if billing_schedule == CustomerPlan.BILLING_SCHEDULE_ANNUAL else 1
flat_discounted_months = min(customer.flat_discounted_months, num_months)
discount = customer.flat_discount * flat_discounted_months
customer.flat_discounted_months -= flat_discounted_months
customer.save(update_fields=["flat_discounted_months"])
stripe.InvoiceItem.create(
currency="usd",
customer=customer.stripe_customer_id,
description=f"${customer.flat_discount}/month new customer discount",
# Negative value to apply discount.
amount=(-1 * discount),
)
if charge_automatically:
collection_method = "charge_automatically"
days_until_due = None
@ -1717,18 +1751,23 @@ class BillingSession(ABC):
billing_frequency = CustomerPlan.BILLING_SCHEDULES[plan.billing_schedule]
if switch_to_annual_at_end_of_cycle:
num_months_next_cycle = 12
annual_price_per_license = get_price_per_license(
plan.tier, CustomerPlan.BILLING_SCHEDULE_ANNUAL, customer.default_discount
)
renewal_cents = annual_price_per_license * licenses_at_next_renewal
price_per_license = format_money(annual_price_per_license / 12)
elif switch_to_monthly_at_end_of_cycle:
num_months_next_cycle = 1
monthly_price_per_license = get_price_per_license(
plan.tier, CustomerPlan.BILLING_SCHEDULE_MONTHLY, customer.default_discount
)
renewal_cents = monthly_price_per_license * licenses_at_next_renewal
price_per_license = format_money(monthly_price_per_license)
else:
num_months_next_cycle = (
12 if plan.billing_schedule == CustomerPlan.BILLING_SCHEDULE_ANNUAL else 1
)
renewal_cents = self.get_customer_plan_renewal_amount(plan, now, last_ledger_entry)
if plan.price_per_license is None:
@ -1738,6 +1777,14 @@ class BillingSession(ABC):
else:
price_per_license = format_money(plan.price_per_license)
# TODO: Do this calculation in `invoice_plan` too.
pre_discount_renewal_cents = renewal_cents
flat_discount, flat_discounted_months = self.get_flat_discount_info(plan.customer)
if flat_discounted_months > 0:
flat_discounted_months = min(flat_discounted_months, num_months_next_cycle)
discount = flat_discount * flat_discounted_months
renewal_cents = renewal_cents - discount
charge_automatically = plan.charge_automatically
assert customer.stripe_customer_id is not None # for mypy
stripe_customer = stripe_get_customer(customer.stripe_customer_id)
@ -1786,6 +1833,9 @@ class BillingSession(ABC):
"legacy_remote_server_next_plan_name": legacy_remote_server_next_plan_name,
"using_min_licenses_for_plan": using_min_licenses_for_plan,
"min_licenses_for_plan": min_licenses_for_plan,
"pre_discount_renewal_cents": cents_to_dollar_string(pre_discount_renewal_cents),
"flat_discount": format_money(customer.flat_discount),
"discounted_months_left": customer.flat_discounted_months,
}
return context
@ -1821,12 +1871,30 @@ class BillingSession(ABC):
"price_per_license",
"discount_percent",
"using_min_licenses_for_plan",
"min_licenses_for_plan",
"pre_discount_renewal_cents",
]
for key in keys:
context[key] = next_plan_context[key]
return context
def get_flat_discount_info(self, customer: Optional[Customer] = None) -> Tuple[int, int]:
is_self_hosted_billing = not isinstance(self, RealmBillingSession)
flat_discount = 0
flat_discounted_months = 0
if is_self_hosted_billing and (customer is None or customer.flat_discounted_months > 0):
if customer is None:
temp_customer = Customer()
flat_discount = temp_customer.flat_discount
flat_discounted_months = 12
else:
flat_discount = customer.flat_discount
flat_discounted_months = customer.flat_discounted_months
assert isinstance(flat_discount, int)
assert isinstance(flat_discounted_months, int)
return flat_discount, flat_discounted_months
def get_initial_upgrade_context(
self, initial_upgrade_request: InitialUpgradeRequest
) -> Tuple[Optional[str], Optional[UpgradePageContext]]:
@ -1893,6 +1961,7 @@ class BillingSession(ABC):
f"{free_trial_end:%B} {free_trial_end.day}, {free_trial_end.year}"
)
flat_discount, flat_discounted_months = self.get_flat_discount_info(customer)
context: UpgradePageContext = {
"customer_name": customer_specific_context["customer_name"],
"default_invoice_days_until_due": DEFAULT_INVOICE_DAYS_UNTIL_DUE,
@ -1917,6 +1986,8 @@ class BillingSession(ABC):
"seat_count": seat_count,
"billing_base_url": self.billing_base_url,
"tier": tier,
"flat_discount": flat_discount,
"flat_discounted_months": flat_discounted_months,
},
"using_min_licenses_for_plan": using_min_licenses_for_plan,
"min_licenses_for_plan": min_licenses_for_plan,
@ -3245,12 +3316,16 @@ class RemoteRealmBillingSession(BillingSession):
remote_realm=self.remote_realm,
defaults={"stripe_customer_id": stripe_customer_id},
)
return customer
else:
customer, created = Customer.objects.update_or_create(
remote_realm=self.remote_realm, defaults=defaults
)
return customer
if created and not customer.default_discount:
customer.flat_discounted_months = 12
customer.save(update_fields=["flat_discounted_months"])
return customer
@override
@transaction.atomic
@ -3634,12 +3709,16 @@ class RemoteServerBillingSession(BillingSession):
remote_server=self.remote_server,
defaults={"stripe_customer_id": stripe_customer_id},
)
return customer
else:
customer, created = Customer.objects.update_or_create(
remote_server=self.remote_server, defaults=defaults
)
return customer
if created and not customer.default_discount:
customer.flat_discounted_months = 12
customer.save(update_fields=["flat_discounted_months"])
return customer
@override
@transaction.atomic

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.8 on 2023-12-19 12:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("corporate", "0030_alter_zulipsponsorshiprequest_requested_plan"),
]
operations = [
migrations.AddField(
model_name="customer",
name="flat_discount",
field=models.IntegerField(default=2000),
),
migrations.AddField(
model_name="customer",
name="flat_discounted_months",
field=models.IntegerField(default=0),
),
]

View File

@ -34,6 +34,11 @@ class Customer(models.Model):
# they purchased.
exempt_from_license_number_check = models.BooleanField(default=False)
# In cents.
flat_discount = models.IntegerField(default=2000)
# Number of months left in the flat discount period.
flat_discounted_months = models.IntegerField(default=0)
class Meta:
# Enforce that at least one of these is set.
constraints = [

View File

@ -579,14 +579,14 @@ class StripeTestCase(ZulipTestCase):
**kwargs: Any,
) -> "TestHttpResponse":
if upgrade_page_response is None:
tier = kwargs.get("tier")
upgrade_url = f"{self.billing_session.billing_base_url}/upgrade/"
if tier:
upgrade_url += f"?tier={tier}"
if self.billing_session.billing_base_url:
upgrade_page_response = self.client_get(
f"{self.billing_session.billing_base_url}/upgrade/", {}, subdomain="selfhosting"
)
upgrade_page_response = self.client_get(upgrade_url, {}, subdomain="selfhosting")
else:
upgrade_page_response = self.client_get(
f"{self.billing_session.billing_base_url}/upgrade/", {}
)
upgrade_page_response = self.client_get(upgrade_url, {})
params: Dict[str, Any] = {
"schedule": "annual",
"signed_seat_count": self.get_signed_seat_count_from_response(upgrade_page_response),
@ -5651,7 +5651,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
@responses.activate
@mock_stripe()
def test_non_sponsorship_billing(self, *mocks: Mock) -> None:
def test_upgrade_user_to_business_plan(self, *mocks: Mock) -> None:
self.login("hamlet")
hamlet = self.example_user("hamlet")
@ -5659,6 +5659,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
realm_user_count = UserProfile.objects.filter(
realm=hamlet.realm, is_bot=False, is_active=True
).count()
self.assertEqual(realm_user_count, 11)
with time_machine.travel(self.now, tick=False):
send_server_data_to_push_bouncer(consider_usage_statistics=False)
@ -5673,7 +5674,24 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
f"{self.billing_session.billing_base_url}/upgrade/", subdomain="selfhosting"
)
self.assertEqual(result.status_code, 200)
self.assert_in_success_response(["Add card", "Purchase Zulip Business"], result)
# Min licenses used since org has less users.
min_licenses = self.billing_session.min_licenses_for_plan(
CustomerPlan.TIER_SELF_HOSTED_BUSINESS
)
self.assertEqual(min_licenses, 25)
flat_discount, flat_discounted_months = self.billing_session.get_flat_discount_info()
self.assertEqual(flat_discounted_months, 12)
self.assert_in_success_response(
[
"Minimum purchase for",
f"{min_licenses} licenses",
"Add card",
"Purchase Zulip Business",
],
result,
)
self.assertFalse(Customer.objects.exists())
self.assertFalse(CustomerPlan.objects.exists())
@ -5693,10 +5711,10 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
for substring in [
"Zulip Business",
"Number of licenses",
f"{realm_user_count} (managed automatically)",
f"{min_licenses} (managed automatically)",
"January 2, 2013",
"Your plan will automatically renew on",
f"${80 * realm_user_count:,.2f}",
f"${80 * min_licenses:,.2f}",
"Visa ending in 4242",
"Update card",
]:
@ -5707,7 +5725,7 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
self.assertEqual(LicenseLedger.objects.count(), 1)
with time_machine.travel(self.now + timedelta(days=2), tick=False):
for count in range(4, 14):
for count in range(realm_user_count, min_licenses + 10):
do_create_user(
f"email {count}",
f"password {count}",
@ -5722,11 +5740,140 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
self.assertEqual(
RemoteRealmAuditLog.objects.count(),
audit_log_count + 10,
min_licenses + 10 - realm_user_count + audit_log_count,
)
latest_ledger = LicenseLedger.objects.last()
assert latest_ledger is not None
self.assertEqual(latest_ledger.licenses, realm_user_count + 10)
self.assertEqual(latest_ledger.licenses, min_licenses + 10)
with time_machine.travel(self.now + timedelta(days=1), tick=False):
response = self.client_get(
f"{self.billing_session.billing_base_url}/billing/", subdomain="selfhosting"
)
self.assertEqual(latest_ledger.licenses, 35)
for substring in [
"Zulip Business",
"Number of licenses",
f"{latest_ledger.licenses} (managed automatically)",
"January 2, 2013",
"Your plan will automatically renew on",
f"${80 * latest_ledger.licenses:,.2f}",
"Visa ending in 4242",
"Update card",
]:
self.assert_in_response(substring, response)
@responses.activate
@mock_stripe()
def test_upgrade_user_to_monthly_basic_plan(self, *mocks: Mock) -> None:
self.login("hamlet")
hamlet = self.example_user("hamlet")
self.add_mock_response()
realm_user_count = UserProfile.objects.filter(
realm=hamlet.realm, is_bot=False, is_active=True
).count()
self.assertEqual(realm_user_count, 11)
with time_machine.travel(self.now, tick=False):
send_server_data_to_push_bouncer(consider_usage_statistics=False)
result = self.execute_remote_billing_authentication_flow(hamlet)
self.assertEqual(result.status_code, 302)
self.assertEqual(result["Location"], f"{self.billing_session.billing_base_url}/plans/")
# upgrade to basic plan
with time_machine.travel(self.now, tick=False):
result = self.client_get(
f"{self.billing_session.billing_base_url}/upgrade/?tier={CustomerPlan.TIER_SELF_HOSTED_BASIC}",
subdomain="selfhosting",
)
self.assertEqual(result.status_code, 200)
min_licenses = self.billing_session.min_licenses_for_plan(
CustomerPlan.TIER_SELF_HOSTED_BASIC
)
self.assertEqual(min_licenses, 10)
flat_discount, flat_discounted_months = self.billing_session.get_flat_discount_info()
self.assertEqual(flat_discounted_months, 12)
self.assert_in_success_response(
[f"{realm_user_count}", "Add card", "Purchase Zulip Basic"], result
)
self.assertFalse(Customer.objects.exists())
self.assertFalse(CustomerPlan.objects.exists())
self.assertFalse(LicenseLedger.objects.exists())
with time_machine.travel(self.now, tick=False):
stripe_customer = self.add_card_and_upgrade(
tier=CustomerPlan.TIER_SELF_HOSTED_BASIC, schedule="monthly"
)
customer = Customer.objects.get(stripe_customer_id=stripe_customer.id)
plan = CustomerPlan.objects.get(customer=customer)
LicenseLedger.objects.get(plan=plan)
with time_machine.travel(self.now + timedelta(days=1), tick=False):
response = self.client_get(
f"{self.billing_session.billing_base_url}/billing/", subdomain="selfhosting"
)
for substring in [
"Zulip Basic",
"Number of licenses",
f"{realm_user_count} (managed automatically)",
"February 2, 2012",
"Your plan will automatically renew on",
f"${3.5 * realm_user_count - flat_discount // 100 * 1:,.2f}",
"Visa ending in 4242",
"Update card",
]:
self.assert_in_response(substring, response)
# Verify that change in user count updates LicenseLedger.
audit_log_count = RemoteRealmAuditLog.objects.count()
self.assertEqual(LicenseLedger.objects.count(), 1)
with time_machine.travel(self.now + timedelta(days=2), tick=False):
for count in range(realm_user_count, min_licenses + 10):
do_create_user(
f"email {count}",
f"password {count}",
hamlet.realm,
"name",
role=UserProfile.ROLE_MEMBER,
acting_user=None,
)
with time_machine.travel(self.now + timedelta(days=3), tick=False):
send_server_data_to_push_bouncer(consider_usage_statistics=False)
self.assertEqual(
RemoteRealmAuditLog.objects.count(),
min_licenses + 10 - realm_user_count + audit_log_count,
)
latest_ledger = LicenseLedger.objects.last()
assert latest_ledger is not None
self.assertEqual(latest_ledger.licenses, min_licenses + 10)
with time_machine.travel(self.now + timedelta(days=1), tick=False):
response = self.client_get(
f"{self.billing_session.billing_base_url}/billing/", subdomain="selfhosting"
)
self.assertEqual(latest_ledger.licenses, 20)
for substring in [
"Zulip Basic",
"Number of licenses",
f"{latest_ledger.licenses} (managed automatically)",
"February 2, 2012",
"Your plan will automatically renew on",
f"${3.5 * latest_ledger.licenses - flat_discount // 100 * 1:,.2f}",
"Visa ending in 4242",
"Update card",
]:
self.assert_in_response(substring, response)
@responses.activate
def test_request_sponsorship(self) -> None:
@ -5918,10 +6065,10 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
for substring in [
"Zulip Business",
"Number of licenses",
f"{server_user_count} (managed automatically)",
f"{25} (managed automatically)",
"Your plan will automatically renew on",
"January 2, 2013",
f"${80 * server_user_count:,.2f}",
f"${80 * 25:,.2f}",
"Visa ending in 4242",
"Update card",
]:
@ -6151,8 +6298,6 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
self.assertEqual(new_customer_plan.tier, CustomerPlan.TIER_SELF_HOSTED_BUSINESS)
self.assertEqual(new_customer_plan.billing_cycle_anchor, end_date)
server_user_count = UserProfile.objects.filter(is_bot=False, is_active=True).count()
# Visit billing page
with time_machine.travel(self.now, tick=False):
response = self.client_get(f"{billing_base_url}/billing/", subdomain="selfhosting")
@ -6160,7 +6305,8 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
"(legacy plan)",
f"This is a legacy plan that ends on {end_date.strftime('%B %d, %Y')}",
f"Your plan will automatically upgrade to Zulip Business on {end_date.strftime('%B %d, %Y')}",
f"Expected charge: <strong>${80 * server_user_count:,.2f}</strong>",
"Expected next charge",
f"${80 * 25 - 20 * 12:,.2f}",
"Visa ending in 4242",
"Update card",
]:
@ -6189,3 +6335,114 @@ class TestRemoteServerBillingFlow(StripeTestCase, RemoteServerTestCase):
m.output[1],
f"INFO:corporate.stripe:Change plan status: Customer.id: {customer.id}, CustomerPlan.id: {customer_plan.id}, status: {CustomerPlan.ACTIVE}",
)
@responses.activate
@mock_stripe()
def test_upgrade_user_to_monthly_basic_plan(self, *mocks: Mock) -> None:
self.login("hamlet")
hamlet = self.example_user("hamlet")
self.add_mock_response()
realm_user_count = UserProfile.objects.filter(is_bot=False, is_active=True).count()
self.assertEqual(realm_user_count, 18)
with time_machine.travel(self.now, tick=False):
send_server_data_to_push_bouncer(consider_usage_statistics=False)
result = self.execute_remote_billing_authentication_flow(
hamlet.delivery_email, hamlet.full_name
)
self.assertEqual(result.status_code, 302)
self.assertEqual(result["Location"], f"{self.billing_session.billing_base_url}/plans/")
# upgrade to basic plan
with time_machine.travel(self.now, tick=False):
result = self.client_get(
f"{self.billing_session.billing_base_url}/upgrade/?tier={CustomerPlan.TIER_SELF_HOSTED_BASIC}",
subdomain="selfhosting",
)
self.assertEqual(result.status_code, 200)
min_licenses = self.billing_session.min_licenses_for_plan(
CustomerPlan.TIER_SELF_HOSTED_BASIC
)
self.assertEqual(min_licenses, 10)
flat_discount, flat_discounted_months = self.billing_session.get_flat_discount_info()
self.assertEqual(flat_discounted_months, 12)
self.assert_in_success_response(
[f"{min_licenses}", "Add card", "Purchase Zulip Basic"], result
)
self.assertFalse(Customer.objects.exists())
self.assertFalse(CustomerPlan.objects.exists())
self.assertFalse(LicenseLedger.objects.exists())
with time_machine.travel(self.now, tick=False):
stripe_customer = self.add_card_and_upgrade(
tier=CustomerPlan.TIER_SELF_HOSTED_BASIC, schedule="monthly"
)
customer = Customer.objects.get(stripe_customer_id=stripe_customer.id)
plan = CustomerPlan.objects.get(customer=customer)
LicenseLedger.objects.get(plan=plan)
with time_machine.travel(self.now + timedelta(days=1), tick=False):
response = self.client_get(
f"{self.billing_session.billing_base_url}/billing/", subdomain="selfhosting"
)
for substring in [
"Zulip Basic",
"Number of licenses",
f"{realm_user_count} (managed automatically)",
"February 2, 2012",
"Your plan will automatically renew on",
f"${3.5 * realm_user_count - flat_discount // 100 * 1:,.2f}",
"Visa ending in 4242",
"Update card",
]:
self.assert_in_response(substring, response)
# Verify that change in user count updates LicenseLedger.
audit_log_count = RemoteRealmAuditLog.objects.count()
self.assertEqual(LicenseLedger.objects.count(), 1)
with time_machine.travel(self.now + timedelta(days=2), tick=False):
for count in range(realm_user_count, min_licenses + 10):
do_create_user(
f"email {count}",
f"password {count}",
hamlet.realm,
"name",
role=UserProfile.ROLE_MEMBER,
acting_user=None,
)
with time_machine.travel(self.now + timedelta(days=3), tick=False):
send_server_data_to_push_bouncer(consider_usage_statistics=False)
self.assertEqual(
RemoteRealmAuditLog.objects.count(),
min_licenses + 10 - realm_user_count + audit_log_count,
)
latest_ledger = LicenseLedger.objects.last()
assert latest_ledger is not None
self.assertEqual(latest_ledger.licenses, min_licenses + 10)
with time_machine.travel(self.now + timedelta(days=1), tick=False):
response = self.client_get(
f"{self.billing_session.billing_base_url}/billing/", subdomain="selfhosting"
)
self.assertEqual(latest_ledger.licenses, 20)
for substring in [
"Zulip Basic",
"Number of licenses",
f"{latest_ledger.licenses} (managed automatically)",
"February 2, 2012",
"Your plan will automatically renew on",
f"${3.5 * latest_ledger.licenses - flat_discount // 100 * 1:,.2f}",
"Visa ending in 4242",
"Update card",
]:
self.assert_in_response(substring, response)

View File

@ -237,25 +237,34 @@
Your next invoice is due on <strong>{{ renewal_date }}</strong>.
{% endif %}
<br />
Expected charge: <strong>${{ renewal_amount }}</strong>
{% if not fixed_price %}
(${{ price_per_license }} x {{ licenses_at_next_renewal }} {{ 'user' if licenses_at_next_renewal == 1 else 'users' }} x
{% if switch_to_annual_at_end_of_cycle %}
12 months
{%- elif switch_to_monthly_at_end_of_cycle %}
1 month
{%- else %}
{{ "1 month" if billing_frequency == "Monthly" else "12 months" }}
{%- endif -%})
{% if discount_percent %}
<br />
<i class="billing-page-discount">Includes: {{ discount_percent }}% discount</i>
<div class="input-box billing-page-field">
<label for="expected-charge">Expected next charge</label>
{% if not fixed_price %}
<div class="not-editable-realm-field">
{% if using_min_licenses_for_plan %}
<i>Minimum purchase for this plan: {{ min_licenses_for_plan }} licenses</i>
<br />
{% endif %}
${{ pre_discount_renewal_cents }} (${{ price_per_license }} x {{ licenses_at_next_renewal }} {{ 'user' if licenses_at_next_renewal == 1 else 'users' }} x
{% if switch_to_annual_at_end_of_cycle %}
12 months
{%- elif switch_to_monthly_at_end_of_cycle %}
1 month
{%- else %}
{{ "1 month" if billing_frequency == "Monthly" else "12 months" }}
{%- endif -%})
{% if discounted_months_left != 0 %}
<br />
Discount: ${{ flat_discount }}/month off <i class="billing-page-discount">({{ discounted_months_left }} {{ "month" if discounted_months_left == 1 else "months" }} remaining)</i>
{% endif %}
{% if discount_percent %}
<br />
<i class="billing-page-discount">Includes: {{ discount_percent }}% discount</i>
{% endif %}
</div>
{% endif %}
{% if using_min_licenses_for_plan %}
<br />
<i>Minimum purchase for this plan: {{ min_licenses_for_plan }} licenses</i>
{% endif %}
{% endif %}
</div>
<h1>${{ renewal_amount }}</h1>
{% endif %}
{% else %}
Your plan ends on <strong>{{ renewal_date }}</strong>, and does not renew.
@ -498,12 +507,12 @@
</header>
<main class="modal__content">
<p>
Your organization will be not be upgraded to <strong>Zulip Business</strong>
Your organization will be not be upgraded to <strong>{{ legacy_remote_server_next_plan_name }}</strong>
on {{ remote_server_legacy_plan_end_date }}. If your organization has more than
10 users at that time, you will lose access to the
<a href="https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html">Mobile Push Notification Service</a>.
You will also not receive the <a href="{{ billing_base_url }}/plans/#self-hosted-plan-comparison">other benefits</a>
of the Zulip Business plan. Are you sure you want to continue?
of the {{ legacy_remote_server_next_plan_name }} plan. Are you sure you want to continue?
</p>
</main>
<footer class="modal__footer">

View File

@ -125,7 +125,12 @@
</span>
</label>
<div id="due-today" class="not-editable-realm-field">
$<span class="due-today-unit-price"></span> x
{% if not manual_license_management and using_min_licenses_for_plan %}
<i>Minimum purchase for this plan: {{ min_licenses_for_plan }} licenses</i>
<br />
{% endif %}
$<span id="pre-discount-renewal-cents"></span>
($<span class="due-today-unit-price"></span> x
{% if not manual_license_management %}
{{ seat_count }}
{% else %}
@ -134,14 +139,14 @@
<span class="due-today-license-count-user-plural">
{{ 'user' if seat_count == 1 else 'users' }}
</span> x
<span class="due-today-duration"></span>
<span class="due-today-duration"></span>)
{% if discount_percent %}
<br/>
<i class="billing-page-discount">Includes: {{ discount_percent }}% discount</i>
{% endif %}
{% if not manual_license_management and using_min_licenses_for_plan %}
<br />
<i>Minimum purchase for this plan: {{ min_licenses_for_plan }} licenses</i>
{% if page_params.flat_discounted_months > 0 %}
<br/>
Discount: $<span class="flat-discounted-price"></span>/month off <i class="billing-page-discount">({{ page_params.flat_discounted_months }} {{ "month" if discounted_months_left == 1 else "months" }} remaining)</i>
{% endif %}
<h1>$<span class="due-today-price"></span></h1>
{% if free_trial_days %}
@ -157,7 +162,7 @@
<p class="not-editable-realm-field">
Your subscription will renew automatically. Your bill will vary based on the number
of active users in your organization. You can also
<a href="{{ page_params.billing_base_url }}/upgrade/?manual_license_management=true&tier%3D{{ page_params.tier }}">purchase a fixed number of licenses</a> instead. See
<a href="{{ page_params.billing_base_url }}/upgrade/?manual_license_management=true&tier={{ page_params.tier }}">purchase a fixed number of licenses</a> instead. See
<a target="_blank" href="https://zulip.com/help/zulip-cloud-billing">here</a> for details.
</p>
<input type="hidden" name="licenses" id="automatic_license_count" value="{{ seat_count }}" />
@ -167,7 +172,7 @@
<p class="not-editable-realm-field">
Your subscription will renew automatically. You will be able to manage the number of licenses on
your organization's billing page. You can also
<a href="{{ page_params.billing_base_url }}/upgrade/?tier%3D{{ page_params.tier }}">choose automatic license management</a> instead. See
<a href="{{ page_params.billing_base_url }}/upgrade/?tier={{ page_params.tier }}">choose automatic license management</a> instead. See
<a href="https://zulip.com/help/zulip-cloud-billing">here</a> for details.
</p>
</div>

View File

@ -9,6 +9,8 @@ export const page_params: {
seat_count: number;
billing_base_url: string;
tier: number;
flat_discount: number;
flat_discounted_months: number;
} = $("#page-params").data("params");
if (!page_params) {

View File

@ -39,9 +39,14 @@ function update_due_today(schedule: string): void {
}
$("#due-today .due-today-duration").text(num_months === 1 ? "1 month" : "12 months");
const schedule_typed = helpers.schedule_schema.parse(schedule);
$(".due-today-price").text(
helpers.format_money(current_license_count * prices[schedule_typed]),
);
const pre_flat_discount_price = prices[schedule_typed] * current_license_count;
$("#pre-discount-renewal-cents").text(helpers.format_money(pre_flat_discount_price));
const flat_discounted_months = Math.min(num_months, page_params.flat_discounted_months);
const total_flat_discount = page_params.flat_discount * flat_discounted_months;
const due_today = Math.max(0, pre_flat_discount_price - total_flat_discount);
$(".flat-discounted-price").text(helpers.format_money(page_params.flat_discount));
$(".due-today-price").text(helpers.format_money(due_today));
const unit_price = prices[schedule_typed] / num_months;
$("#due-today .due-today-unit-price").text(helpers.format_money(unit_price));
}