From dca7e654ca96d62b0f706dcffa71c213f172f0fc Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Sat, 9 Dec 2023 07:16:53 +0000 Subject: [PATCH] billing: Enforce min license for plan on upgrade and billing page. --- corporate/lib/stripe.py | 32 ++++++++++++++++++++++++++++---- templates/corporate/billing.html | 9 ++++++--- templates/corporate/upgrade.html | 6 +++++- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index 3c719eca9a..2b2c09db5e 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -184,8 +184,9 @@ def validate_licenses( licenses: Optional[int], seat_count: int, exempt_from_license_number_check: bool, + min_licenses_for_plan: int, ) -> None: - min_licenses = seat_count + min_licenses = max(seat_count, min_licenses_for_plan) max_licenses = None if not charge_automatically: min_licenses = max(seat_count, MIN_INVOICED_LICENSES) @@ -214,6 +215,7 @@ def check_upgrade_parameters( licenses: Optional[int], seat_count: int, exempt_from_license_number_check: bool, + min_licenses_for_plan: int, ) -> None: if billing_modality not in VALID_BILLING_MODALITY_VALUES: # nocoverage raise BillingError("unknown billing_modality", "") @@ -226,6 +228,7 @@ def check_upgrade_parameters( licenses, seat_count, exempt_from_license_number_check, + min_licenses_for_plan, ) @@ -633,6 +636,7 @@ class UpgradePageContext(TypedDict): free_trial_end_date: Optional[str] is_demo_organization: bool manual_license_management: bool + using_min_licenses_for_plan: bool page_params: UpgradePageParams payment_method: Optional[str] plan: str @@ -1287,6 +1291,7 @@ class BillingSession(ABC): licenses, seat_count, exempt_from_license_number_check, + self.min_licenses_for_plan(upgrade_request.tier), ) assert licenses is not None and license_management is not None automanage_licenses = license_management == "automatic" @@ -1612,7 +1617,12 @@ class BillingSession(ABC): licenses = last_ledger_entry.licenses licenses_at_next_renewal = last_ledger_entry.licenses_at_next_renewal 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() + 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 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, "remote_server_legacy_plan_end_date": remote_server_legacy_plan_end_date, "legacy_remote_server_new_plan_name": legacy_remote_server_new_plan_name, + "using_min_licenses_for_plan": using_min_licenses_for_plan, } return context @@ -1727,6 +1738,7 @@ class BillingSession(ABC): "fixed_price", "price_per_license", "discount_percent", + "using_min_licenses_for_plan", ] for key in keys: @@ -1767,10 +1779,14 @@ class BillingSession(ABC): # 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 - 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 + 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_end_date = None @@ -1809,6 +1825,7 @@ class BillingSession(ABC): "seat_count": seat_count, "billing_base_url": self.billing_base_url, }, + "using_min_licenses_for_plan": using_min_licenses_for_plan, "payment_method": current_payment_method, "plan": CustomerPlan.name_from_tier(tier), "salt": salt, @@ -1819,6 +1836,11 @@ class BillingSession(ABC): 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: if plan is None: # nocoverage # TODO: Add test coverage. Right now, this logic is used @@ -1955,6 +1977,7 @@ class BillingSession(ABC): licenses, self.current_count_for_billed_licenses(), 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) return @@ -1987,6 +2010,7 @@ class BillingSession(ABC): licenses_at_next_renewal, self.current_count_for_billed_licenses(), 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_at_next_renewal=licenses_at_next_renewal diff --git a/templates/corporate/billing.html b/templates/corporate/billing.html index dab28c765c..a9b4b272f4 100644 --- a/templates/corporate/billing.html +++ b/templates/corporate/billing.html @@ -193,11 +193,14 @@ (${{ 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 %} + {%- elif switch_to_monthly_at_end_of_cycle %} 1 month - {% else %} + {%- else %} {{ "1 month" if billing_frequency == "Monthly" else "12 months" }} - {% endif %}) + {%- endif -%}) + {% if using_min_licenses_for_plan %} + (minimum purchase) + {% endif %} {% if discount_percent %}
Includes {{ discount_percent }}% discount. diff --git a/templates/corporate/upgrade.html b/templates/corporate/upgrade.html index 35fe75ffdc..bfeac3aac9 100644 --- a/templates/corporate/upgrade.html +++ b/templates/corporate/upgrade.html @@ -125,8 +125,12 @@ {{ 'user' if seat_count == 1 else 'users' }} x + {% if not manual_license_management and using_min_licenses_for_plan %} + (minimum purchase) + {% endif %} {% if discount_percent %} - (includes {{ discount_percent }}% discount) +
+ Includes {{ discount_percent }}% discount. {% endif %}

$

{% if free_trial_days %}