billing: Enforce min license for plan on upgrade and billing page.

This commit is contained in:
Aman Agrawal 2023-12-09 07:16:53 +00:00 committed by Tim Abbott
parent 67d4e8456d
commit dca7e654ca
3 changed files with 39 additions and 8 deletions

View File

@ -184,8 +184,9 @@ def validate_licenses(
licenses: Optional[int], licenses: Optional[int],
seat_count: int, seat_count: int,
exempt_from_license_number_check: bool, exempt_from_license_number_check: bool,
min_licenses_for_plan: int,
) -> None: ) -> None:
min_licenses = seat_count min_licenses = max(seat_count, min_licenses_for_plan)
max_licenses = None max_licenses = None
if not charge_automatically: if not charge_automatically:
min_licenses = max(seat_count, MIN_INVOICED_LICENSES) min_licenses = max(seat_count, MIN_INVOICED_LICENSES)
@ -214,6 +215,7 @@ def check_upgrade_parameters(
licenses: Optional[int], licenses: Optional[int],
seat_count: int, seat_count: int,
exempt_from_license_number_check: bool, exempt_from_license_number_check: bool,
min_licenses_for_plan: int,
) -> None: ) -> None:
if billing_modality not in VALID_BILLING_MODALITY_VALUES: # nocoverage if billing_modality not in VALID_BILLING_MODALITY_VALUES: # nocoverage
raise BillingError("unknown billing_modality", "") raise BillingError("unknown billing_modality", "")
@ -226,6 +228,7 @@ def check_upgrade_parameters(
licenses, licenses,
seat_count, seat_count,
exempt_from_license_number_check, exempt_from_license_number_check,
min_licenses_for_plan,
) )
@ -633,6 +636,7 @@ class UpgradePageContext(TypedDict):
free_trial_end_date: Optional[str] free_trial_end_date: Optional[str]
is_demo_organization: bool is_demo_organization: bool
manual_license_management: bool manual_license_management: bool
using_min_licenses_for_plan: bool
page_params: UpgradePageParams page_params: UpgradePageParams
payment_method: Optional[str] payment_method: Optional[str]
plan: str plan: str
@ -1287,6 +1291,7 @@ class BillingSession(ABC):
licenses, licenses,
seat_count, seat_count,
exempt_from_license_number_check, exempt_from_license_number_check,
self.min_licenses_for_plan(upgrade_request.tier),
) )
assert licenses is not None and license_management is not None assert licenses is not None and license_management is not None
automanage_licenses = license_management == "automatic" automanage_licenses = license_management == "automatic"
@ -1612,7 +1617,12 @@ class BillingSession(ABC):
licenses = last_ledger_entry.licenses licenses = last_ledger_entry.licenses
licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal
assert licenses_at_next_renewal is not None assert licenses_at_next_renewal is not None
min_licenses_for_plan = self.min_licenses_for_plan(plan.tier)
seat_count = self.current_count_for_billed_licenses() seat_count = self.current_count_for_billed_licenses()
using_min_licenses_for_plan = (
min_licenses_for_plan == licenses_at_next_renewal
and licenses_at_next_renewal > seat_count
)
# Should do this in JavaScript, using the user's time zone # Should do this in JavaScript, using the user's time zone
if plan.is_free_trial() or downgrade_at_end_of_free_trial: if plan.is_free_trial() or downgrade_at_end_of_free_trial:
@ -1693,6 +1703,7 @@ class BillingSession(ABC):
"is_server_on_legacy_plan": remote_server_legacy_plan_end_date is not None, "is_server_on_legacy_plan": remote_server_legacy_plan_end_date is not None,
"remote_server_legacy_plan_end_date": remote_server_legacy_plan_end_date, "remote_server_legacy_plan_end_date": remote_server_legacy_plan_end_date,
"legacy_remote_server_new_plan_name": legacy_remote_server_new_plan_name, "legacy_remote_server_new_plan_name": legacy_remote_server_new_plan_name,
"using_min_licenses_for_plan": using_min_licenses_for_plan,
} }
return context return context
@ -1727,6 +1738,7 @@ class BillingSession(ABC):
"fixed_price", "fixed_price",
"price_per_license", "price_per_license",
"discount_percent", "discount_percent",
"using_min_licenses_for_plan",
] ]
for key in keys: for key in keys:
@ -1767,10 +1779,14 @@ class BillingSession(ABC):
# Show "Update card" button if user has already added a card. # Show "Update card" button if user has already added a card.
current_payment_method = None if "ending in" not in payment_method else payment_method current_payment_method = None if "ending in" not in payment_method else payment_method
customer_specific_context = self.get_upgrade_page_session_type_specific_context()
seat_count = self.current_count_for_billed_licenses()
signed_seat_count, salt = sign_string(str(seat_count))
tier = initial_upgrade_request.tier tier = initial_upgrade_request.tier
customer_specific_context = self.get_upgrade_page_session_type_specific_context()
min_licenses_for_plan = self.min_licenses_for_plan(tier)
seat_count = self.current_count_for_billed_licenses()
using_min_licenses_for_plan = min_licenses_for_plan > seat_count
if using_min_licenses_for_plan:
seat_count = min_licenses_for_plan
signed_seat_count, salt = sign_string(str(seat_count))
free_trial_days = None free_trial_days = None
free_trial_end_date = None free_trial_end_date = None
@ -1809,6 +1825,7 @@ class BillingSession(ABC):
"seat_count": seat_count, "seat_count": seat_count,
"billing_base_url": self.billing_base_url, "billing_base_url": self.billing_base_url,
}, },
"using_min_licenses_for_plan": using_min_licenses_for_plan,
"payment_method": current_payment_method, "payment_method": current_payment_method,
"plan": CustomerPlan.name_from_tier(tier), "plan": CustomerPlan.name_from_tier(tier),
"salt": salt, "salt": salt,
@ -1819,6 +1836,11 @@ class BillingSession(ABC):
return None, context return None, context
def min_licenses_for_plan(self, tier: int) -> int:
if tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS:
return 10
return 1
def downgrade_at_the_end_of_billing_cycle(self, plan: Optional[CustomerPlan] = None) -> None: def downgrade_at_the_end_of_billing_cycle(self, plan: Optional[CustomerPlan] = None) -> None:
if plan is None: # nocoverage if plan is None: # nocoverage
# TODO: Add test coverage. Right now, this logic is used # TODO: Add test coverage. Right now, this logic is used
@ -1955,6 +1977,7 @@ class BillingSession(ABC):
licenses, licenses,
self.current_count_for_billed_licenses(), self.current_count_for_billed_licenses(),
plan.customer.exempt_from_license_number_check, plan.customer.exempt_from_license_number_check,
self.min_licenses_for_plan(plan.tier),
) )
self.update_license_ledger_for_manual_plan(plan, timezone_now(), licenses=licenses) self.update_license_ledger_for_manual_plan(plan, timezone_now(), licenses=licenses)
return return
@ -1987,6 +2010,7 @@ class BillingSession(ABC):
licenses_at_next_renewal, licenses_at_next_renewal,
self.current_count_for_billed_licenses(), self.current_count_for_billed_licenses(),
plan.customer.exempt_from_license_number_check, plan.customer.exempt_from_license_number_check,
self.min_licenses_for_plan(plan.tier),
) )
self.update_license_ledger_for_manual_plan( self.update_license_ledger_for_manual_plan(
plan, timezone_now(), licenses_at_next_renewal=licenses_at_next_renewal plan, timezone_now(), licenses_at_next_renewal=licenses_at_next_renewal

View File

@ -193,11 +193,14 @@
(${{ price_per_license }} x {{ licenses_at_next_renewal }} {{ 'user' if licenses_at_next_renewal == 1 else 'users' }} x (${{ 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 %} {% if switch_to_annual_at_end_of_cycle %}
12 months 12 months
{% elif switch_to_monthly_at_end_of_cycle %} {%- elif switch_to_monthly_at_end_of_cycle %}
1 month 1 month
{% else %} {%- else %}
{{ "1 month" if billing_frequency == "Monthly" else "12 months" }} {{ "1 month" if billing_frequency == "Monthly" else "12 months" }}
{% endif %}) {%- endif -%})
{% if using_min_licenses_for_plan %}
(<i>minimum purchase</i>)
{% endif %}
{% if discount_percent %} {% if discount_percent %}
<br /> <br />
<i class="billing-page-discount">Includes {{ discount_percent }}% discount.</i> <i class="billing-page-discount">Includes {{ discount_percent }}% discount.</i>

View File

@ -125,8 +125,12 @@
{{ 'user' if seat_count == 1 else 'users' }} {{ 'user' if seat_count == 1 else 'users' }}
</span> x </span> x
<span class="due-today-duration"></span> <span class="due-today-duration"></span>
{% if not manual_license_management and using_min_licenses_for_plan %}
<i>(minimum purchase)</i>
{% endif %}
{% if discount_percent %} {% if discount_percent %}
<i class="billing-page-discount">(includes {{ discount_percent }}% discount)</i> <br/>
<i class="billing-page-discount">Includes {{ discount_percent }}% discount.</i>
{% endif %} {% endif %}
<h1>$<span class="due-today-price"></span></h1> <h1>$<span class="due-today-price"></span></h1>
{% if free_trial_days %} {% if free_trial_days %}