billing: Allow user to switch between billing frequencies.

This commit is contained in:
Aman Agrawal 2023-11-20 12:01:25 +00:00 committed by Tim Abbott
parent 6d80460425
commit 69d8442ab4
42 changed files with 399 additions and 21 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
},
});
});
}
$(() => {

View File

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

View File

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