mirror of https://github.com/zulip/zulip.git
support: Add updating minimum licenses requirement after discount.
Adds a support action for updating the minimum licenses on a customer object once a default discount has also been set. In the case that the current billing entity has a current active plan or a scheduled upgrade to a new plan, then the minimum licenses will not be updated.
This commit is contained in:
parent
deaf734488
commit
fb29a35262
|
@ -409,6 +409,7 @@ def remote_servers_support(
|
|||
remote_server_id: Optional[int] = REQ(default=None, converter=to_non_negative_int),
|
||||
remote_realm_id: Optional[int] = REQ(default=None, converter=to_non_negative_int),
|
||||
discount: Optional[Decimal] = REQ(default=None, converter=to_decimal),
|
||||
minimum_licenses: Optional[int] = REQ(default=None, converter=to_non_negative_int),
|
||||
sponsorship_pending: Optional[bool] = REQ(default=None, json_validator=check_bool),
|
||||
approve_sponsorship: bool = REQ(default=False, json_validator=check_bool),
|
||||
billing_modality: Optional[str] = REQ(
|
||||
|
@ -458,6 +459,11 @@ def remote_servers_support(
|
|||
support_type=SupportType.attach_discount,
|
||||
discount=discount,
|
||||
)
|
||||
elif minimum_licenses is not None:
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.update_minimum_licenses,
|
||||
minimum_licenses=minimum_licenses,
|
||||
)
|
||||
elif billing_modality is not None:
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.update_billing_modality,
|
||||
|
|
|
@ -553,6 +553,7 @@ class SupportType(Enum):
|
|||
attach_discount = 3
|
||||
update_billing_modality = 4
|
||||
modify_plan = 5
|
||||
update_minimum_licenses = 6
|
||||
|
||||
|
||||
class SupportViewRequest(TypedDict, total=False):
|
||||
|
@ -562,6 +563,7 @@ class SupportViewRequest(TypedDict, total=False):
|
|||
billing_modality: Optional[str]
|
||||
plan_modification: Optional[str]
|
||||
new_plan_tier: Optional[int]
|
||||
minimum_licenses: Optional[int]
|
||||
|
||||
|
||||
class AuditLogEventType(Enum):
|
||||
|
@ -575,6 +577,7 @@ class AuditLogEventType(Enum):
|
|||
CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 8
|
||||
CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN = 9
|
||||
BILLING_ENTITY_PLAN_TYPE_CHANGED = 10
|
||||
MINIMUM_LICENSES_CHANGED = 11
|
||||
|
||||
|
||||
class PlanTierChangeType(Enum):
|
||||
|
@ -1085,6 +1088,46 @@ class BillingSession(ABC):
|
|||
)
|
||||
return f"Discount for {self.billing_entity_display_name} changed to {new_discount_string}% from {old_discount_string}%."
|
||||
|
||||
def update_customer_minimum_licenses(self, new_minimum_license_count: int) -> str:
|
||||
previous_minimum_license_count = None
|
||||
customer = self.get_customer()
|
||||
|
||||
# Currently, the support admin view shows the form for adding
|
||||
# a minimum license count after a default discount has been set.
|
||||
assert customer is not None
|
||||
if customer.default_discount is None or int(customer.default_discount) == 0:
|
||||
raise SupportRequestError(
|
||||
f"Discount for {self.billing_entity_display_name} must be updated before setting a minimum number of licenses."
|
||||
)
|
||||
|
||||
plan = get_current_plan_by_customer(customer)
|
||||
if plan is not None and plan.tier != CustomerPlan.TIER_SELF_HOSTED_LEGACY:
|
||||
raise SupportRequestError(
|
||||
f"Cannot set minimum licenses; active plan already exists for {self.billing_entity_display_name}."
|
||||
)
|
||||
|
||||
next_plan = self.get_legacy_remote_server_next_plan(customer)
|
||||
if next_plan is not None: # nocoverage
|
||||
raise SupportRequestError(
|
||||
f"Cannot set minimum licenses; upgrade to new plan already scheduled for {self.billing_entity_display_name}."
|
||||
)
|
||||
|
||||
previous_minimum_license_count = customer.minimum_licenses
|
||||
customer.minimum_licenses = new_minimum_license_count
|
||||
customer.save(update_fields=["minimum_licenses"])
|
||||
|
||||
self.write_to_audit_log(
|
||||
event_type=AuditLogEventType.MINIMUM_LICENSES_CHANGED,
|
||||
event_time=timezone_now(),
|
||||
extra_data={
|
||||
"old_minimum_licenses": previous_minimum_license_count,
|
||||
"new_minimum_licenses": new_minimum_license_count,
|
||||
},
|
||||
)
|
||||
if previous_minimum_license_count is None:
|
||||
previous_minimum_license_count = 0
|
||||
return f"Minimum licenses for {self.billing_entity_display_name} changed to {new_minimum_license_count} from {previous_minimum_license_count}."
|
||||
|
||||
def update_customer_sponsorship_status(self, sponsorship_pending: bool) -> str:
|
||||
customer = self.get_customer()
|
||||
if customer is None:
|
||||
|
@ -2027,6 +2070,11 @@ class BillingSession(ABC):
|
|||
return None, context
|
||||
|
||||
def min_licenses_for_plan(self, tier: int) -> int:
|
||||
customer = self.get_customer()
|
||||
if customer is not None and customer.minimum_licenses:
|
||||
assert customer.default_discount is not None
|
||||
return customer.minimum_licenses
|
||||
|
||||
if tier == CustomerPlan.TIER_SELF_HOSTED_BASIC:
|
||||
return 10
|
||||
if tier == CustomerPlan.TIER_SELF_HOSTED_BUSINESS:
|
||||
|
@ -2587,6 +2635,10 @@ class BillingSession(ABC):
|
|||
assert support_request["discount"] is not None
|
||||
new_discount = support_request["discount"]
|
||||
success_message = self.attach_discount_to_customer(new_discount)
|
||||
elif support_type == SupportType.update_minimum_licenses:
|
||||
assert support_request["minimum_licenses"] is not None
|
||||
new_minimum_license_count = support_request["minimum_licenses"]
|
||||
success_message = self.update_customer_minimum_licenses(new_minimum_license_count)
|
||||
elif support_type == SupportType.update_billing_modality:
|
||||
assert support_request["billing_modality"] is not None
|
||||
assert support_request["billing_modality"] in VALID_BILLING_MODALITY_VALUES
|
||||
|
@ -2909,6 +2961,8 @@ class RealmBillingSession(BillingSession):
|
|||
return RealmAuditLog.CUSTOMER_PLAN_CREATED
|
||||
elif event_type is AuditLogEventType.DISCOUNT_CHANGED:
|
||||
return RealmAuditLog.REALM_DISCOUNT_CHANGED
|
||||
elif event_type is AuditLogEventType.MINIMUM_LICENSES_CHANGED:
|
||||
return RealmAuditLog.CUSTOMER_MINIMUM_LICENSES_CHANGED
|
||||
elif event_type is AuditLogEventType.SPONSORSHIP_APPROVED:
|
||||
return RealmAuditLog.REALM_SPONSORSHIP_APPROVED
|
||||
elif event_type is AuditLogEventType.SPONSORSHIP_PENDING_STATUS_CHANGED:
|
||||
|
@ -3265,6 +3319,8 @@ class RemoteRealmBillingSession(BillingSession):
|
|||
return RemoteRealmAuditLog.CUSTOMER_PLAN_CREATED
|
||||
elif event_type is AuditLogEventType.DISCOUNT_CHANGED: # nocoverage
|
||||
return RemoteRealmAuditLog.REMOTE_SERVER_DISCOUNT_CHANGED
|
||||
elif event_type is AuditLogEventType.MINIMUM_LICENSES_CHANGED:
|
||||
return RemoteRealmAuditLog.CUSTOMER_MINIMUM_LICENSES_CHANGED # nocoverage
|
||||
elif event_type is AuditLogEventType.SPONSORSHIP_APPROVED:
|
||||
return RemoteRealmAuditLog.REMOTE_SERVER_SPONSORSHIP_APPROVED
|
||||
elif event_type is AuditLogEventType.SPONSORSHIP_PENDING_STATUS_CHANGED:
|
||||
|
@ -3675,6 +3731,8 @@ class RemoteServerBillingSession(BillingSession):
|
|||
return RemoteZulipServerAuditLog.CUSTOMER_PLAN_CREATED
|
||||
elif event_type is AuditLogEventType.DISCOUNT_CHANGED:
|
||||
return RemoteZulipServerAuditLog.REMOTE_SERVER_DISCOUNT_CHANGED # nocoverage
|
||||
elif event_type is AuditLogEventType.MINIMUM_LICENSES_CHANGED:
|
||||
return RemoteZulipServerAuditLog.CUSTOMER_MINIMUM_LICENSES_CHANGED # nocoverage
|
||||
elif event_type is AuditLogEventType.SPONSORSHIP_APPROVED:
|
||||
return RemoteZulipServerAuditLog.REMOTE_SERVER_SPONSORSHIP_APPROVED
|
||||
elif event_type is AuditLogEventType.SPONSORSHIP_PENDING_STATUS_CHANGED:
|
||||
|
|
|
@ -44,6 +44,7 @@ class SponsorshipRequestDict(TypedDict):
|
|||
class SponsorshipData:
|
||||
sponsorship_pending: bool = False
|
||||
default_discount: Optional[Decimal] = None
|
||||
minimum_licenses: Optional[int] = None
|
||||
latest_sponsorship_request: Optional[SponsorshipRequestDict] = None
|
||||
|
||||
|
||||
|
@ -89,6 +90,7 @@ def get_customer_discount_for_support_view(
|
|||
def get_customer_sponsorship_data(customer: Customer) -> SponsorshipData:
|
||||
pending = customer.sponsorship_pending
|
||||
discount = customer.default_discount
|
||||
licenses = customer.minimum_licenses
|
||||
sponsorship_request = None
|
||||
if pending:
|
||||
last_sponsorship_request = (
|
||||
|
@ -116,6 +118,7 @@ def get_customer_sponsorship_data(customer: Customer) -> SponsorshipData:
|
|||
return SponsorshipData(
|
||||
sponsorship_pending=pending,
|
||||
default_discount=discount,
|
||||
minimum_licenses=licenses,
|
||||
latest_sponsorship_request=sponsorship_request,
|
||||
)
|
||||
|
||||
|
|
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.
|
@ -5402,6 +5402,50 @@ class TestSupportBillingHelpers(StripeTestCase):
|
|||
):
|
||||
billing_session.approve_sponsorship()
|
||||
|
||||
@mock_stripe()
|
||||
def test_add_minimum_licenses(self, *mocks: Mock) -> None:
|
||||
min_licenses = 25
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.update_minimum_licenses, minimum_licenses=min_licenses
|
||||
)
|
||||
support_admin = self.example_user("iago")
|
||||
user = self.example_user("hamlet")
|
||||
billing_session = RealmBillingSession(support_admin, realm=user.realm, support_session=True)
|
||||
|
||||
billing_session.update_or_create_customer()
|
||||
with self.assertRaisesRegex(
|
||||
SupportRequestError,
|
||||
"Discount for zulip must be updated before setting a minimum number of licenses.",
|
||||
):
|
||||
billing_session.process_support_view_request(support_view_request)
|
||||
|
||||
billing_session.attach_discount_to_customer(Decimal(50))
|
||||
message = billing_session.process_support_view_request(support_view_request)
|
||||
self.assertEqual("Minimum licenses for zulip changed to 25 from 0.", message)
|
||||
realm_audit_log = RealmAuditLog.objects.filter(
|
||||
event_type=RealmAuditLog.CUSTOMER_MINIMUM_LICENSES_CHANGED
|
||||
).last()
|
||||
assert realm_audit_log is not None
|
||||
expected_extra_data = {"old_minimum_licenses": None, "new_minimum_licenses": 25}
|
||||
self.assertEqual(realm_audit_log.extra_data, expected_extra_data)
|
||||
|
||||
self.login_user(user)
|
||||
self.add_card_and_upgrade(user)
|
||||
customer = billing_session.get_customer()
|
||||
assert customer is not None
|
||||
[charge] = iter(stripe.Charge.list(customer=customer.stripe_customer_id))
|
||||
self.assertEqual(4000 * min_licenses, charge.amount)
|
||||
|
||||
min_licenses = 50
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.update_minimum_licenses, minimum_licenses=min_licenses
|
||||
)
|
||||
with self.assertRaisesRegex(
|
||||
SupportRequestError,
|
||||
"Cannot set minimum licenses; active plan already exists for zulip.",
|
||||
):
|
||||
billing_session.process_support_view_request(support_view_request)
|
||||
|
||||
def test_approve_realm_sponsorship(self) -> None:
|
||||
realm = get_realm("zulip")
|
||||
self.assertNotEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD_FREE)
|
||||
|
|
|
@ -33,6 +33,17 @@
|
|||
{% endif %}
|
||||
</form>
|
||||
|
||||
{% if not has_fixed_price and (sponsorship_data.default_discount or sponsorship_data.minimum_licenses) %}
|
||||
<form method="POST" class="remote-form">
|
||||
<b>Minimum licenses</b>:<br />
|
||||
{{ csrf_input }}
|
||||
<input type="hidden" name="{{ remote_type }}" value="{{ remote_id }}" />
|
||||
<input type="number" name="minimum_licenses" value="{{ sponsorship_data.minimum_licenses }}" required />
|
||||
<button type="submit" class="btn btn-default support-submit-button">Update</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if sponsorship_data.sponsorship_pending %}
|
||||
<div class="">
|
||||
<h4>Sponsorship request information:</h4>
|
||||
|
|
|
@ -101,6 +101,7 @@ class AbstractRealmAuditLog(models.Model):
|
|||
CUSTOMER_PLAN_CREATED = 502
|
||||
CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 503
|
||||
CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN = 504
|
||||
CUSTOMER_MINIMUM_LICENSES_CHANGED = 505
|
||||
|
||||
STREAM_CREATED = 601
|
||||
STREAM_DEACTIVATED = 602
|
||||
|
|
Loading…
Reference in New Issue