mirror of https://github.com/zulip/zulip.git
billing: Allow user to switch between billing frequencies.
This commit is contained in:
parent
6d80460425
commit
69d8442ab4
|
@ -461,6 +461,7 @@ class AuditLogEventType(Enum):
|
|||
SPONSORSHIP_PENDING_STATUS_CHANGED = 6
|
||||
BILLING_METHOD_CHANGED = 7
|
||||
CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 8
|
||||
CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN = 9
|
||||
|
||||
|
||||
class BillingSessionAuditLogEventError(Exception):
|
||||
|
@ -1015,6 +1016,54 @@ class BillingSession(ABC):
|
|||
)
|
||||
return new_plan, new_plan_ledger_entry
|
||||
|
||||
if plan.status == CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE:
|
||||
if plan.fixed_price is not None: # nocoverage
|
||||
raise BillingError("Customer is already on monthly fixed plan.")
|
||||
|
||||
plan.status = CustomerPlan.ENDED
|
||||
plan.save(update_fields=["status"])
|
||||
|
||||
discount = plan.customer.default_discount or plan.discount
|
||||
_, _, _, price_per_license = compute_plan_parameters(
|
||||
tier=plan.tier,
|
||||
automanage_licenses=plan.automanage_licenses,
|
||||
billing_schedule=CustomerPlan.MONTHLY,
|
||||
discount=plan.discount,
|
||||
)
|
||||
|
||||
new_plan = CustomerPlan.objects.create(
|
||||
customer=plan.customer,
|
||||
billing_schedule=CustomerPlan.MONTHLY,
|
||||
automanage_licenses=plan.automanage_licenses,
|
||||
charge_automatically=plan.charge_automatically,
|
||||
price_per_license=price_per_license,
|
||||
discount=discount,
|
||||
billing_cycle_anchor=next_billing_cycle,
|
||||
tier=plan.tier,
|
||||
status=CustomerPlan.ACTIVE,
|
||||
next_invoice_date=next_billing_cycle,
|
||||
invoiced_through=None,
|
||||
invoicing_status=CustomerPlan.INITIAL_INVOICE_TO_BE_SENT,
|
||||
)
|
||||
|
||||
new_plan_ledger_entry = LicenseLedger.objects.create(
|
||||
plan=new_plan,
|
||||
is_renewal=True,
|
||||
event_time=next_billing_cycle,
|
||||
licenses=licenses_at_next_renewal,
|
||||
licenses_at_next_renewal=licenses_at_next_renewal,
|
||||
)
|
||||
|
||||
self.write_to_audit_log(
|
||||
event_type=AuditLogEventType.CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN,
|
||||
event_time=event_time,
|
||||
extra_data={
|
||||
"annual_plan_id": plan.id,
|
||||
"monthly_plan_id": new_plan.id,
|
||||
},
|
||||
)
|
||||
return new_plan, new_plan_ledger_entry
|
||||
|
||||
if plan.status == CustomerPlan.SWITCH_NOW_FROM_STANDARD_TO_PLUS:
|
||||
standard_plan = plan
|
||||
standard_plan.end_date = next_billing_cycle
|
||||
|
@ -1079,8 +1128,12 @@ class BillingSession(ABC):
|
|||
switch_to_annual_at_end_of_cycle = (
|
||||
plan.status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE
|
||||
)
|
||||
switch_to_monthly_at_end_of_cycle = (
|
||||
plan.status == CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE
|
||||
)
|
||||
licenses = last_ledger_entry.licenses
|
||||
licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal
|
||||
assert licenses_at_next_renewal is not None
|
||||
seat_count = self.current_count_for_billed_licenses()
|
||||
|
||||
# Should do this in JavaScript, using the user's time zone
|
||||
|
@ -1092,7 +1145,30 @@ class BillingSession(ABC):
|
|||
dt=start_of_next_billing_cycle(plan, now)
|
||||
)
|
||||
|
||||
renewal_cents = renewal_amount(plan, now, last_ledger_entry)
|
||||
billing_frequency = CustomerPlan.BILLING_SCHEDULES[plan.billing_schedule]
|
||||
|
||||
if switch_to_annual_at_end_of_cycle:
|
||||
annual_price_per_license = get_price_per_license(
|
||||
plan.tier, CustomerPlan.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:
|
||||
monthly_price_per_license = get_price_per_license(
|
||||
plan.tier, CustomerPlan.MONTHLY, customer.default_discount
|
||||
)
|
||||
renewal_cents = monthly_price_per_license * licenses_at_next_renewal
|
||||
price_per_license = format_money(monthly_price_per_license)
|
||||
else:
|
||||
renewal_cents = renewal_amount(plan, now, last_ledger_entry)
|
||||
|
||||
if plan.price_per_license is None:
|
||||
price_per_license = ""
|
||||
elif billing_frequency == "Annual":
|
||||
price_per_license = format_money(plan.price_per_license / 12)
|
||||
else:
|
||||
price_per_license = format_money(plan.price_per_license)
|
||||
|
||||
charge_automatically = plan.charge_automatically
|
||||
assert customer.stripe_customer_id is not None # for mypy
|
||||
stripe_customer = stripe_get_customer(customer.stripe_customer_id)
|
||||
|
@ -1107,15 +1183,6 @@ class BillingSession(ABC):
|
|||
else None
|
||||
)
|
||||
|
||||
billing_frequency = CustomerPlan.BILLING_SCHEDULES[plan.billing_schedule]
|
||||
|
||||
if plan.price_per_license is None:
|
||||
price_per_license = ""
|
||||
elif billing_frequency == "Annual":
|
||||
price_per_license = format_money(plan.price_per_license / 12)
|
||||
else:
|
||||
price_per_license = format_money(plan.price_per_license)
|
||||
|
||||
context = {
|
||||
"plan_name": plan.name,
|
||||
"has_active_plan": True,
|
||||
|
@ -1123,6 +1190,7 @@ class BillingSession(ABC):
|
|||
"downgrade_at_end_of_cycle": downgrade_at_end_of_cycle,
|
||||
"automanage_licenses": plan.automanage_licenses,
|
||||
"switch_to_annual_at_end_of_cycle": switch_to_annual_at_end_of_cycle,
|
||||
"switch_to_monthly_at_end_of_cycle": switch_to_monthly_at_end_of_cycle,
|
||||
"licenses": licenses,
|
||||
"licenses_at_next_renewal": licenses_at_next_renewal,
|
||||
"seat_count": seat_count,
|
||||
|
@ -1245,6 +1313,8 @@ class RealmBillingSession(BillingSession):
|
|||
return RealmAuditLog.REALM_BILLING_METHOD_CHANGED
|
||||
elif event_type is AuditLogEventType.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN:
|
||||
return RealmAuditLog.CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN
|
||||
elif event_type is AuditLogEventType.CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN:
|
||||
return RealmAuditLog.CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN
|
||||
else:
|
||||
raise BillingSessionAuditLogEventError(event_type)
|
||||
|
||||
|
|
|
@ -260,6 +260,7 @@ class CustomerPlan(models.Model):
|
|||
FREE_TRIAL = 3
|
||||
SWITCH_TO_ANNUAL_AT_END_OF_CYCLE = 4
|
||||
SWITCH_NOW_FROM_STANDARD_TO_PLUS = 5
|
||||
SWITCH_TO_MONTHLY_AT_END_OF_CYCLE = 6
|
||||
# "Live" plans should have a value < LIVE_STATUS_THRESHOLD.
|
||||
# There should be at most one live plan per customer.
|
||||
LIVE_STATUS_THRESHOLD = 10
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -2628,6 +2628,182 @@ class StripeTest(StripeTestCase):
|
|||
for key, value in annual_plan_invoice_item_params.items():
|
||||
self.assertEqual(invoice_item[key], value)
|
||||
|
||||
@mock_stripe()
|
||||
def test_switch_from_annual_plan_to_monthly_plan_for_automatic_license_management(
|
||||
self, *mocks: Mock
|
||||
) -> None:
|
||||
user = self.example_user("hamlet")
|
||||
self.login_user(user)
|
||||
self.add_card_and_upgrade(user, schedule="annual")
|
||||
annual_plan = get_current_plan_by_realm(user.realm)
|
||||
assert annual_plan is not None
|
||||
self.assertEqual(annual_plan.automanage_licenses, True)
|
||||
self.assertEqual(annual_plan.billing_schedule, CustomerPlan.ANNUAL)
|
||||
|
||||
stripe_customer_id = Customer.objects.get(realm=user.realm).id
|
||||
new_plan = get_current_plan_by_realm(user.realm)
|
||||
assert new_plan is not None
|
||||
|
||||
with self.assertLogs("corporate.stripe", "INFO") as m:
|
||||
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
||||
response = self.client_patch(
|
||||
"/json/billing/plan",
|
||||
{"status": CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE},
|
||||
)
|
||||
expected_log = f"INFO:corporate.stripe:Change plan status: Customer.id: {stripe_customer_id}, CustomerPlan.id: {new_plan.id}, status: {CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE}"
|
||||
self.assertEqual(m.output[0], expected_log)
|
||||
self.assert_json_success(response)
|
||||
annual_plan.refresh_from_db()
|
||||
self.assertEqual(annual_plan.status, CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE)
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
response = self.client_get("/billing/")
|
||||
self.assert_in_success_response(
|
||||
["Your plan will switch to monthly billing on January 2, 2013"], response
|
||||
)
|
||||
|
||||
with patch("corporate.lib.stripe.get_latest_seat_count", return_value=20):
|
||||
update_license_ledger_if_needed(user.realm, self.now)
|
||||
self.assertEqual(LicenseLedger.objects.filter(plan=annual_plan).count(), 2)
|
||||
self.assertEqual(
|
||||
LicenseLedger.objects.order_by("-id")
|
||||
.values_list("licenses", "licenses_at_next_renewal")
|
||||
.first(),
|
||||
(20, 20),
|
||||
)
|
||||
|
||||
# Check that we don't switch to monthly plan at next invoice date (which is used to charge user for
|
||||
# additional licenses) but at the end of current billing cycle.
|
||||
self.assertEqual(annual_plan.next_invoice_date, self.next_month)
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=annual_plan.next_invoice_date):
|
||||
with patch("corporate.lib.stripe.get_latest_seat_count", return_value=25):
|
||||
assert annual_plan.next_invoice_date is not None
|
||||
update_license_ledger_if_needed(user.realm, annual_plan.next_invoice_date)
|
||||
|
||||
annual_plan.refresh_from_db()
|
||||
self.assertEqual(annual_plan.status, CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE)
|
||||
self.assertEqual(annual_plan.next_invoice_date, self.next_month)
|
||||
self.assertEqual(annual_plan.billing_schedule, CustomerPlan.ANNUAL)
|
||||
self.assertEqual(LicenseLedger.objects.filter(plan=annual_plan).count(), 3)
|
||||
|
||||
invoice_plans_as_needed(self.next_month + timedelta(days=1))
|
||||
|
||||
annual_plan.refresh_from_db()
|
||||
self.assertEqual(annual_plan.next_invoice_date, add_months(self.next_month, 1))
|
||||
self.assertEqual(annual_plan.invoicing_status, CustomerPlan.DONE)
|
||||
self.assertEqual(LicenseLedger.objects.filter(plan=annual_plan).count(), 3)
|
||||
|
||||
customer = get_customer_by_realm(user.realm)
|
||||
assert customer is not None
|
||||
assert customer.stripe_customer_id
|
||||
[invoice0, invoice1] = iter(stripe.Invoice.list(customer=customer.stripe_customer_id))
|
||||
[invoice_item1, invoice_item2] = iter(invoice0.lines)
|
||||
annual_plan_invoice_item_params = {
|
||||
"amount": 7322 * 5,
|
||||
"description": "Additional license (Feb 2, 2012 - Jan 2, 2013)",
|
||||
"plan": None,
|
||||
"quantity": 5,
|
||||
"subscription": None,
|
||||
"discountable": False,
|
||||
"period": {
|
||||
"start": datetime_to_timestamp(self.next_month),
|
||||
"end": datetime_to_timestamp(self.next_year),
|
||||
},
|
||||
}
|
||||
|
||||
for key, value in annual_plan_invoice_item_params.items():
|
||||
self.assertEqual(invoice_item1[key], value)
|
||||
|
||||
annual_plan_invoice_item_params = {
|
||||
"amount": 14 * 80 * 1 * 100,
|
||||
"description": "Additional license (Jan 2, 2012 - Jan 2, 2013)",
|
||||
"plan": None,
|
||||
"quantity": 14,
|
||||
"subscription": None,
|
||||
"discountable": False,
|
||||
"period": {
|
||||
"start": datetime_to_timestamp(self.now),
|
||||
"end": datetime_to_timestamp(self.next_year),
|
||||
},
|
||||
}
|
||||
|
||||
for key, value in annual_plan_invoice_item_params.items():
|
||||
self.assertEqual(invoice_item2[key], value)
|
||||
|
||||
# Check that we switch to monthly plan at the end of current billing cycle.
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.next_year):
|
||||
with patch("corporate.lib.stripe.get_latest_seat_count", return_value=25):
|
||||
update_license_ledger_if_needed(user.realm, self.next_year)
|
||||
self.assertEqual(LicenseLedger.objects.filter(plan=annual_plan).count(), 3)
|
||||
customer = get_customer_by_realm(user.realm)
|
||||
assert customer is not None
|
||||
annual_plan.refresh_from_db()
|
||||
self.assertEqual(annual_plan.status, CustomerPlan.ENDED)
|
||||
self.assertEqual(annual_plan.next_invoice_date, add_months(self.next_month, 1))
|
||||
monthly_plan = get_current_plan_by_realm(user.realm)
|
||||
assert monthly_plan is not None
|
||||
self.assertEqual(monthly_plan.status, CustomerPlan.ACTIVE)
|
||||
self.assertEqual(monthly_plan.billing_schedule, CustomerPlan.MONTHLY)
|
||||
self.assertEqual(monthly_plan.invoicing_status, CustomerPlan.INITIAL_INVOICE_TO_BE_SENT)
|
||||
self.assertEqual(monthly_plan.billing_cycle_anchor, self.next_year)
|
||||
self.assertEqual(monthly_plan.next_invoice_date, self.next_year)
|
||||
self.assertEqual(monthly_plan.invoiced_through, None)
|
||||
monthly_ledger_entries = LicenseLedger.objects.filter(plan=monthly_plan).order_by("id")
|
||||
self.assert_length(monthly_ledger_entries, 2)
|
||||
self.assertEqual(monthly_ledger_entries[0].is_renewal, True)
|
||||
self.assertEqual(
|
||||
monthly_ledger_entries.values_list("licenses", "licenses_at_next_renewal")[0], (25, 25)
|
||||
)
|
||||
self.assertEqual(monthly_ledger_entries[1].is_renewal, False)
|
||||
self.assertEqual(
|
||||
monthly_ledger_entries.values_list("licenses", "licenses_at_next_renewal")[1], (25, 25)
|
||||
)
|
||||
audit_log = RealmAuditLog.objects.get(
|
||||
event_type=RealmAuditLog.CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN
|
||||
)
|
||||
self.assertEqual(audit_log.realm, user.realm)
|
||||
self.assertEqual(audit_log.extra_data["annual_plan_id"], annual_plan.id)
|
||||
self.assertEqual(audit_log.extra_data["monthly_plan_id"], monthly_plan.id)
|
||||
|
||||
invoice_plans_as_needed(self.next_year)
|
||||
|
||||
monthly_ledger_entries = LicenseLedger.objects.filter(plan=monthly_plan).order_by("id")
|
||||
self.assert_length(monthly_ledger_entries, 2)
|
||||
monthly_plan.refresh_from_db()
|
||||
self.assertEqual(monthly_plan.invoicing_status, CustomerPlan.DONE)
|
||||
self.assertEqual(monthly_plan.invoiced_through, monthly_ledger_entries[1])
|
||||
self.assertEqual(monthly_plan.billing_cycle_anchor, self.next_year)
|
||||
self.assertEqual(monthly_plan.next_invoice_date, add_months(self.next_year, 1))
|
||||
annual_plan.refresh_from_db()
|
||||
self.assertEqual(annual_plan.next_invoice_date, None)
|
||||
|
||||
assert customer.stripe_customer_id
|
||||
[invoice0, invoice1, invoice2] = iter(
|
||||
stripe.Invoice.list(customer=customer.stripe_customer_id)
|
||||
)
|
||||
|
||||
[invoice_item0] = iter(invoice0.lines)
|
||||
|
||||
monthly_plan_invoice_item_params = {
|
||||
"amount": 25 * 8 * 100,
|
||||
"description": "Zulip Cloud Standard - renewal",
|
||||
"plan": None,
|
||||
"quantity": 25,
|
||||
"subscription": None,
|
||||
"discountable": False,
|
||||
"period": {
|
||||
"start": datetime_to_timestamp(self.next_year),
|
||||
"end": datetime_to_timestamp(add_months(self.next_year, 1)),
|
||||
},
|
||||
}
|
||||
for key, value in monthly_plan_invoice_item_params.items():
|
||||
self.assertEqual(invoice_item0[key], value)
|
||||
|
||||
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
||||
response = self.client_get("/billing/")
|
||||
self.assert_not_in_success_response(
|
||||
["Your plan will switch to annual billing on February 2, 2012"], response
|
||||
)
|
||||
|
||||
def test_reupgrade_after_plan_status_changed_to_downgrade_at_end_of_cycle(self) -> None:
|
||||
user = self.example_user("hamlet")
|
||||
self.login_user(user)
|
||||
|
|
|
@ -132,6 +132,7 @@ def update_plan(
|
|||
CustomerPlan.ACTIVE,
|
||||
CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE,
|
||||
CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE,
|
||||
CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE,
|
||||
CustomerPlan.ENDED,
|
||||
]
|
||||
),
|
||||
|
@ -160,14 +161,23 @@ def update_plan(
|
|||
|
||||
if status is not None:
|
||||
if status == CustomerPlan.ACTIVE:
|
||||
assert plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE
|
||||
assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
do_change_plan_status(plan, status)
|
||||
elif status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
|
||||
assert plan.status == CustomerPlan.ACTIVE
|
||||
assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
downgrade_at_the_end_of_billing_cycle(user.realm)
|
||||
elif status == CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE:
|
||||
assert plan.billing_schedule == CustomerPlan.MONTHLY
|
||||
assert plan.status == CustomerPlan.ACTIVE
|
||||
assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
# Customer needs to switch to an active plan first to avoid unexpected behavior.
|
||||
assert plan.status != CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE
|
||||
assert plan.fixed_price is None
|
||||
do_change_plan_status(plan, status)
|
||||
elif status == CustomerPlan.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE:
|
||||
assert plan.billing_schedule == CustomerPlan.ANNUAL
|
||||
assert plan.status < CustomerPlan.LIVE_STATUS_THRESHOLD
|
||||
# Customer needs to switch to an active plan first to avoid unexpected behavior.
|
||||
assert plan.status != CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE
|
||||
assert plan.fixed_price is None
|
||||
do_change_plan_status(plan, status)
|
||||
elif status == CustomerPlan.ENDED:
|
||||
|
|
|
@ -29,15 +29,44 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-box billing-page-field no-validation">
|
||||
<label for="org-billing-frequency" class="inline-block label-title">Billing frequency</label>
|
||||
<div id="org-billing-frequency" class="not-editable-realm-field">
|
||||
<div class="input-box billing-page-field no-validation org-billing-frequency-wrapper"
|
||||
data-current-billing-frequency="{{ billing_frequency }}"
|
||||
{%if free_trial %}data-free-trial="true"{% endif %}
|
||||
{%if downgrade_at_end_of_cycle %}data-downgrade-eoc="true"{% endif %}
|
||||
{%if switch_to_monthly_at_end_of_cycle %}data-switch-to-monthly-eoc="true"{% endif %}
|
||||
{%if switch_to_annual_at_end_of_cycle %}data-switch-to-annual-eoc="true"{% endif %}>
|
||||
<label for="org-billing-frequency">Billing frequency</label>
|
||||
{% if free_trial or downgrade_at_end_of_cycle %}
|
||||
<div class="not-editable-realm-field">
|
||||
{{ billing_frequency }}
|
||||
{% if switch_to_annual_at_end_of_cycle %}
|
||||
<br />
|
||||
Your plan will switch to annual billing on {{ renewal_date }}.
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif switch_to_annual_at_end_of_cycle %}
|
||||
<select name="schedule" id="org-billing-frequency-annual" class="billing-frequency-select">
|
||||
<option value="Monthly">Monthly</option>
|
||||
<option value="Annual" selected>Annual</option>
|
||||
</select>
|
||||
<div class="billing-frequency-message not-editable-realm-field">
|
||||
Your plan will switch to annual billing on {{ renewal_date }}.
|
||||
</div>
|
||||
{%elif switch_to_monthly_at_end_of_cycle %}
|
||||
<select name="schedule" id="org-billing-frequency-monthly" class="billing-frequency-select">
|
||||
<option value="Monthly" selected>Monthly</option>
|
||||
<option value="Annual">Annual</option>
|
||||
</select>
|
||||
<div class="billing-frequency-message not-editable-realm-field">
|
||||
Your plan will switch to monthly billing on {{ renewal_date }}.
|
||||
</div>
|
||||
{% else %}
|
||||
<select name="schedule" id="org-billing-frequency-default" class="billing-frequency-select">
|
||||
<option value="Monthly" {% if billing_frequency == "Monthly" %}selected{% endif %}>Monthly</option>
|
||||
<option value="Annual" {% if billing_frequency == "Annual" %}selected{% endif %}>Annual</option>
|
||||
</select>
|
||||
{% endif %}
|
||||
<button id="org-billing-frequency-confirm-button" class="hide">
|
||||
<span class="billing-button-text">Update</span>
|
||||
<object class="loader billing-button-loader" type="image/svg+xml" data="{{ static('images/loading/loader-white.svg') }}"></object>
|
||||
</button>
|
||||
<div id="org-billing-frequency-change-error" class="alert alert-danger billing-page-error"></div>
|
||||
</div>
|
||||
{% if automanage_licenses %}
|
||||
<div class="input-box billing-page-field no-validation">
|
||||
|
@ -130,7 +159,14 @@
|
|||
<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 {{ "1 month" if billing_frequency == "Monthly" else "12 months" }})
|
||||
(${{ 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 %})
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
|
|
|
@ -1,9 +1,20 @@
|
|||
import $ from "jquery";
|
||||
import {z} from "zod";
|
||||
|
||||
import * as portico_modals from "../portico/portico_modals";
|
||||
|
||||
import * as helpers from "./helpers";
|
||||
|
||||
const billing_frequency_schema = z.enum(["Monthly", "Annual"]);
|
||||
|
||||
enum CustomerPlanStatus {
|
||||
ACTIVE = 1,
|
||||
DOWNGRADE_AT_END_OF_CYCLE = 2,
|
||||
FREE_TRIAL = 3,
|
||||
SWITCH_TO_ANNUAL_AT_END_OF_CYCLE = 4,
|
||||
SWITCH_TO_MONTHLY_AT_END_OF_CYCLE = 6,
|
||||
}
|
||||
|
||||
export function create_update_current_cycle_license_request(): void {
|
||||
$("#current-manual-license-count-update-button .billing-button-text").text("");
|
||||
$("#current-manual-license-count-update-button .loader").show();
|
||||
|
@ -255,6 +266,61 @@ export function initialize(): void {
|
|||
}
|
||||
}, 300); // Wait for 300ms after the user stops typing
|
||||
});
|
||||
|
||||
$<HTMLInputElement>(".billing-frequency-select").on("change", function () {
|
||||
const $wrapper = $(".org-billing-frequency-wrapper");
|
||||
const switch_to_annual_eoc = $wrapper.attr("data-switch-to-annual-eoc") === "true";
|
||||
const switch_to_monthly_eoc = $wrapper.attr("data-switch-to-monthly-eoc") === "true";
|
||||
const free_trial = $wrapper.attr("data-free-trial") === "true";
|
||||
const downgrade_at_end_of_cycle = $wrapper.attr("data-downgrade-eoc") === "true";
|
||||
const current_billing_frequency = $wrapper.attr("data-current-billing-frequency");
|
||||
const billing_frequency_selected = billing_frequency_schema.parse(this.value);
|
||||
|
||||
if (
|
||||
(switch_to_annual_eoc && billing_frequency_selected === "Monthly") ||
|
||||
(switch_to_monthly_eoc && billing_frequency_selected === "Annual")
|
||||
) {
|
||||
$("#org-billing-frequency-confirm-button").toggleClass("hide", false);
|
||||
let new_status = CustomerPlanStatus.ACTIVE;
|
||||
if (downgrade_at_end_of_cycle) {
|
||||
new_status = CustomerPlanStatus.DOWNGRADE_AT_END_OF_CYCLE;
|
||||
} else if (free_trial) {
|
||||
new_status = CustomerPlanStatus.FREE_TRIAL;
|
||||
}
|
||||
$("#org-billing-frequency-confirm-button").attr("data-status", new_status);
|
||||
} else if (current_billing_frequency !== billing_frequency_selected) {
|
||||
$("#org-billing-frequency-confirm-button").toggleClass("hide", false);
|
||||
let new_status = CustomerPlanStatus.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE;
|
||||
if (billing_frequency_selected === "Monthly") {
|
||||
new_status = CustomerPlanStatus.SWITCH_TO_MONTHLY_AT_END_OF_CYCLE;
|
||||
}
|
||||
$("#org-billing-frequency-confirm-button").attr("data-status", new_status);
|
||||
} else {
|
||||
$("#org-billing-frequency-confirm-button").toggleClass("hide", true);
|
||||
}
|
||||
});
|
||||
|
||||
$("#org-billing-frequency-confirm-button").on("click", (e) => {
|
||||
e.preventDefault();
|
||||
void $.ajax({
|
||||
type: "patch",
|
||||
url: "/json/billing/plan",
|
||||
data: {
|
||||
status: $("#org-billing-frequency-confirm-button").attr("data-status"),
|
||||
},
|
||||
success() {
|
||||
window.location.replace(
|
||||
"/billing/?success_message=" +
|
||||
encodeURIComponent("Billing frequency has been updated."),
|
||||
);
|
||||
},
|
||||
error(xhr) {
|
||||
if (xhr.responseJSON?.msg) {
|
||||
$("#org-billing-frequency-change-error").text(xhr.responseJSON.msg);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$(() => {
|
||||
|
|
|
@ -458,6 +458,7 @@ input[name="licenses"] {
|
|||
bottom: 15px;
|
||||
}
|
||||
|
||||
#billing-page #org-billing-frequency-confirm-button,
|
||||
#billing-page .license-count-update-button {
|
||||
margin: 0 auto;
|
||||
font-size: 1.1rem;
|
||||
|
@ -465,10 +466,19 @@ input[name="licenses"] {
|
|||
width: 100px;
|
||||
}
|
||||
|
||||
#billing-page #org-billing-frequency-confirm-button.hide,
|
||||
#billing-page .license-count-update-button.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#billing-page #org-billing-frequency-confirm-button {
|
||||
margin: 0;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 25px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#billing-page #current-license-change-form,
|
||||
#billing-page #next-license-change-form {
|
||||
margin-bottom: 0;
|
||||
|
@ -535,6 +545,7 @@ input[name="licenses"] {
|
|||
}
|
||||
}
|
||||
|
||||
#billing-page-details .billing-frequency-message.not-editable-realm-field,
|
||||
#upgrade-page-details #onboarding-free-trial-not-ready,
|
||||
#onboarding-go-to-org .not-editable-realm-field,
|
||||
#free-trial-top-banner .not-editable-realm-field,
|
||||
|
@ -592,6 +603,13 @@ input[name="licenses"] {
|
|||
margin-right: 0;
|
||||
}
|
||||
|
||||
#billing-page-details
|
||||
.org-billing-frequency-wrapper.input-box
|
||||
.billing-frequency-select {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
#billing-page-details .org-billing-frequency-wrapper.billing-page-field,
|
||||
#upgrade-page-details .upgrade-add-card-container {
|
||||
text-align: left;
|
||||
}
|
||||
|
|
|
@ -4782,6 +4782,7 @@ class AbstractRealmAuditLog(models.Model):
|
|||
CUSTOMER_CREATED = 501
|
||||
CUSTOMER_PLAN_CREATED = 502
|
||||
CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 503
|
||||
CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN = 504
|
||||
|
||||
STREAM_CREATED = 601
|
||||
STREAM_DEACTIVATED = 602
|
||||
|
|
Loading…
Reference in New Issue