billing: Highlight manual license management errors for admins.

In the billing portal UI for manual license management, limit
decreasing the number of licenses for the next billing period to
be less than the currently used licenses. If the customer is exempt
from license number checks, then this limit is not applied.

Also, visually highlight manual license management errors so that
the billing admin is aware of potential issues.

As of these changes, current licenses can be under the seat count
when a guest user is changed to a non-guest user. And next billing
period licenses can be under the seat cout when a user joins with a
currently available, purchased license after a billing admin has
decreased the number of licenses set for the next billing period.
This commit is contained in:
Lauryn Menard 2024-09-05 18:42:53 +02:00 committed by Tim Abbott
parent b02cf53d5e
commit 137f4fccde
3 changed files with 73 additions and 12 deletions

View File

@ -2545,6 +2545,7 @@ class BillingSession(ABC):
"licenses": licenses, "licenses": licenses,
"licenses_at_next_renewal": licenses_at_next_renewal, "licenses_at_next_renewal": licenses_at_next_renewal,
"seat_count": seat_count, "seat_count": seat_count,
"exempt_from_license_number_check": customer.exempt_from_license_number_check,
"renewal_date": renewal_date, "renewal_date": renewal_date,
"renewal_amount": cents_to_dollar_string(renewal_cents) if renewal_cents != 0 else None, "renewal_amount": cents_to_dollar_string(renewal_cents) if renewal_cents != 0 else None,
"payment_method": payment_method, "payment_method": payment_method,

View File

@ -143,7 +143,7 @@
</label> </label>
<div class="number-input-with-label"> <div class="number-input-with-label">
<form id="current-license-change-form"> <form id="current-license-change-form">
<input type="number" name="licenses" autocomplete="off" id="current-manual-license-count" class="short-width-number-input" data-original-value="{{ licenses }}" value="{{ licenses }}" required/> <input type="number" name="licenses" autocomplete="off" id="current-manual-license-count" class="short-width-number-input" data-original-value="{{ licenses }}" value="{{ licenses }}" {%if not exempt_from_license_number_check %}min="{{ seat_count }}"{% endif %} required/>
</form> </form>
<span class="licence-count-in-use">licenses ({{ seat_count }} in use)</span> <span class="licence-count-in-use">licenses ({{ seat_count }} in use)</span>
<button id="current-manual-license-count-update-button" class="license-count-update-button hide"> <button id="current-manual-license-count-update-button" class="license-count-update-button hide">
@ -170,7 +170,7 @@
</label> </label>
<div class="number-input-with-label"> <div class="number-input-with-label">
<form id="next-license-change-form"> <form id="next-license-change-form">
<input type="number" name="licenses_at_next_renewal" autocomplete="off" id="next-manual-license-count" class="short-width-number-input" data-original-value="{{ licenses_at_next_renewal }}" value="{{ licenses_at_next_renewal }}" required/> <input type="number" name="licenses_at_next_renewal" autocomplete="off" id="next-manual-license-count" class="short-width-number-input" data-original-value="{{ licenses_at_next_renewal }}" value="{{ licenses_at_next_renewal }}" {%if not exempt_from_license_number_check %}min="{{ seat_count }}"{% endif %} required/>
</form> </form>
<span class="licence-count-in-use">licenses ({{ seat_count }} in use)</span> <span class="licence-count-in-use">licenses ({{ seat_count }} in use)</span>
<button id="next-manual-license-count-update-button" class="license-count-update-button hide"> <button id="next-manual-license-count-update-button" class="license-count-update-button hide">

View File

@ -101,9 +101,10 @@ export function initialize(): void {
e.preventDefault(); e.preventDefault();
}); });
function get_old_and_new_license_count_for_current_cycle(): { function get_license_counts_for_current_cycle(): {
new_current_manual_license_count: number; new_current_manual_license_count: number;
old_current_manual_license_count: number; old_current_manual_license_count: number;
min_current_manual_license_count: number;
} { } {
const new_current_manual_license_count: number = Number.parseInt( const new_current_manual_license_count: number = Number.parseInt(
$<HTMLInputElement>("input#current-manual-license-count").val()!, $<HTMLInputElement>("input#current-manual-license-count").val()!,
@ -113,15 +114,25 @@ export function initialize(): void {
$<HTMLInputElement>("input#current-manual-license-count").attr("data-original-value")!, $<HTMLInputElement>("input#current-manual-license-count").attr("data-original-value")!,
10, 10,
); );
let min_current_manual_license_count: number = Number.parseInt(
$<HTMLInputElement>("input#current-manual-license-count").attr("min")!,
10,
);
if (Number.isNaN(min_current_manual_license_count)) {
// Customer is exempt from license number checks.
min_current_manual_license_count = 0;
}
return { return {
new_current_manual_license_count, new_current_manual_license_count,
old_current_manual_license_count, old_current_manual_license_count,
min_current_manual_license_count,
}; };
} }
function get_old_and_new_license_count_for_next_cycle(): { function get_license_counts_for_next_cycle(): {
new_next_manual_license_count: number; new_next_manual_license_count: number;
old_next_manual_license_count: number; old_next_manual_license_count: number;
min_next_manual_license_count: number;
} { } {
const new_next_manual_license_count: number = Number.parseInt( const new_next_manual_license_count: number = Number.parseInt(
$<HTMLInputElement>("input#next-manual-license-count").val()!, $<HTMLInputElement>("input#next-manual-license-count").val()!,
@ -131,12 +142,43 @@ export function initialize(): void {
$<HTMLInputElement>("input#next-manual-license-count").attr("data-original-value")!, $<HTMLInputElement>("input#next-manual-license-count").attr("data-original-value")!,
10, 10,
); );
let min_next_manual_license_count: number = Number.parseInt(
$<HTMLInputElement>("input#next-manual-license-count").attr("min")!,
10,
);
if (Number.isNaN(min_next_manual_license_count)) {
// Customer is exempt from license number checks.
min_next_manual_license_count = 0;
}
return { return {
new_next_manual_license_count, new_next_manual_license_count,
old_next_manual_license_count, old_next_manual_license_count,
min_next_manual_license_count,
}; };
} }
function check_for_manual_billing_errors(): void {
const {old_next_manual_license_count, min_next_manual_license_count} =
get_license_counts_for_next_cycle();
if (old_next_manual_license_count < min_next_manual_license_count) {
$("#next-license-change-error").text(
"Number of licenses for next billing period less than licenses in use.",
);
} else {
$("#next-license-change-error").text("");
}
const {old_current_manual_license_count, min_current_manual_license_count} =
get_license_counts_for_current_cycle();
if (old_current_manual_license_count < min_current_manual_license_count) {
$("#current-license-change-error").text(
"Number of licenses for current billing period less than licenses in use.",
);
} else {
$("#current-license-change-error").text("");
}
}
$("#current-license-change-form, #next-license-change-form").on("submit", (e) => { $("#current-license-change-form, #next-license-change-form").on("submit", (e) => {
// We don't want user to accidentally update the license count on pressing enter. // We don't want user to accidentally update the license count on pressing enter.
e.preventDefault(); e.preventDefault();
@ -149,7 +191,7 @@ export function initialize(): void {
} }
e.preventDefault(); e.preventDefault();
const {new_current_manual_license_count, old_current_manual_license_count} = const {new_current_manual_license_count, old_current_manual_license_count} =
get_old_and_new_license_count_for_current_cycle(); get_license_counts_for_current_cycle();
const $modal = $("#confirm-licenses-modal-increase"); const $modal = $("#confirm-licenses-modal-increase");
$modal.find(".new_license_count_holder").text(new_current_manual_license_count); $modal.find(".new_license_count_holder").text(new_current_manual_license_count);
$modal.find(".current_license_count_holder").text(old_current_manual_license_count); $modal.find(".current_license_count_holder").text(old_current_manual_license_count);
@ -166,7 +208,7 @@ export function initialize(): void {
} }
e.preventDefault(); e.preventDefault();
const {new_next_manual_license_count, old_next_manual_license_count} = const {new_next_manual_license_count, old_next_manual_license_count} =
get_old_and_new_license_count_for_next_cycle(); get_license_counts_for_next_cycle();
let $modal; let $modal;
if (new_next_manual_license_count > old_next_manual_license_count) { if (new_next_manual_license_count > old_next_manual_license_count) {
$modal = $("#confirm-licenses-modal-increase"); $modal = $("#confirm-licenses-modal-increase");
@ -290,15 +332,23 @@ export function initialize(): void {
let timeout: ReturnType<typeof setTimeout> | null = null; let timeout: ReturnType<typeof setTimeout> | null = null;
check_for_manual_billing_errors();
$("#current-manual-license-count").on("keyup", () => { $("#current-manual-license-count").on("keyup", () => {
if (timeout !== null) { if (timeout !== null) {
clearTimeout(timeout); clearTimeout(timeout);
} }
timeout = setTimeout(() => { timeout = setTimeout(() => {
const {new_current_manual_license_count, old_current_manual_license_count} = const {
get_old_and_new_license_count_for_current_cycle(); new_current_manual_license_count,
if (new_current_manual_license_count > old_current_manual_license_count) { old_current_manual_license_count,
min_current_manual_license_count,
} = get_license_counts_for_current_cycle();
if (
new_current_manual_license_count > old_current_manual_license_count &&
new_current_manual_license_count > min_current_manual_license_count
) {
$("#current-manual-license-count-update-button").toggleClass("hide", false); $("#current-manual-license-count-update-button").toggleClass("hide", false);
$("#current-license-change-error").text(""); $("#current-license-change-error").text("");
} else if (new_current_manual_license_count < old_current_manual_license_count) { } else if (new_current_manual_license_count < old_current_manual_license_count) {
@ -308,7 +358,7 @@ export function initialize(): void {
$("#current-manual-license-count-update-button").toggleClass("hide", true); $("#current-manual-license-count-update-button").toggleClass("hide", true);
} else { } else {
$("#current-manual-license-count-update-button").toggleClass("hide", true); $("#current-manual-license-count-update-button").toggleClass("hide", true);
$("#current-license-change-error").text(""); check_for_manual_billing_errors();
} }
}, 300); // Wait for 300ms after the user stops typing }, 300); // Wait for 300ms after the user stops typing
}); });
@ -319,16 +369,26 @@ export function initialize(): void {
} }
timeout = setTimeout(() => { timeout = setTimeout(() => {
const {new_next_manual_license_count, old_next_manual_license_count} = const {
get_old_and_new_license_count_for_next_cycle(); new_next_manual_license_count,
old_next_manual_license_count,
min_next_manual_license_count,
} = get_license_counts_for_next_cycle();
if ( if (
!new_next_manual_license_count || !new_next_manual_license_count ||
new_next_manual_license_count < 0 || new_next_manual_license_count < 0 ||
new_next_manual_license_count === old_next_manual_license_count new_next_manual_license_count === old_next_manual_license_count
) { ) {
$("#next-manual-license-count-update-button").toggleClass("hide", true); $("#next-manual-license-count-update-button").toggleClass("hide", true);
check_for_manual_billing_errors();
} else if (new_next_manual_license_count < min_next_manual_license_count) {
$("#next-manual-license-count-update-button").toggleClass("hide", true);
$("#next-license-change-error").text(
"Cannot be less than the number of licenses currently in use.",
);
} else { } else {
$("#next-manual-license-count-update-button").toggleClass("hide", false); $("#next-manual-license-count-update-button").toggleClass("hide", false);
$("#next-license-change-error").text("");
} }
}, 300); // Wait for 300ms after the user stops typing }, 300); // Wait for 300ms after the user stops typing
}); });