mirror of https://github.com/zulip/zulip.git
analytics: Create process_support_view_request BillingSession method.
Creates a process_support_view_request method for BillingSession to process the various support requests that relate to the billing system. Moves approve_realm_sponsorship, update_realm_sponsorship_status, and attach_discount_to_realm to this new BillingSession method. Adds a new abstract property to BillingSession to have a string value, billing_entity_display_name, to use for support messages sent when these requests are processed.
This commit is contained in:
parent
0679bc044a
commit
5d25cab42b
|
@ -1,4 +1,5 @@
|
|||
from datetime import datetime, timedelta, timezone
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
from unittest import mock
|
||||
|
||||
|
@ -7,8 +8,7 @@ import time_machine
|
|||
from django.utils.timezone import now as timezone_now
|
||||
from typing_extensions import override
|
||||
|
||||
from corporate.lib.stripe import add_months
|
||||
from corporate.lib.support import update_realm_sponsorship_status
|
||||
from corporate.lib.stripe import RealmBillingSession, add_months
|
||||
from corporate.models import Customer, CustomerPlan, LicenseLedger, get_customer_by_realm
|
||||
from zerver.actions.invites import do_create_multiuse_invite_link
|
||||
from zerver.actions.realm_settings import do_change_realm_org_type, do_send_realm_reactivation_email
|
||||
|
@ -533,14 +533,15 @@ class TestSupportEndpoint(ZulipTestCase):
|
|||
self.assertEqual(result["Location"], "/login/")
|
||||
|
||||
iago = self.example_user("iago")
|
||||
self.login("iago")
|
||||
self.login_user(iago)
|
||||
|
||||
with mock.patch("analytics.views.support.attach_discount_to_realm") as m:
|
||||
result = self.client_post(
|
||||
"/activity/support", {"realm_id": f"{lear_realm.id}", "discount": "25"}
|
||||
)
|
||||
m.assert_called_once_with(get_realm("lear"), 25, acting_user=iago)
|
||||
self.assert_in_success_response(["Discount of lear changed to 25% from 0%"], result)
|
||||
self.assert_in_success_response(["Discount for lear changed to 25% from 0%"], result)
|
||||
customer = get_customer_by_realm(lear_realm)
|
||||
assert customer is not None
|
||||
self.assertEqual(customer.default_discount, Decimal(25))
|
||||
|
||||
def test_change_sponsorship_status(self) -> None:
|
||||
lear_realm = get_realm("lear")
|
||||
|
@ -577,7 +578,10 @@ class TestSupportEndpoint(ZulipTestCase):
|
|||
def test_approve_sponsorship(self) -> None:
|
||||
support_admin = self.example_user("iago")
|
||||
lear_realm = get_realm("lear")
|
||||
update_realm_sponsorship_status(lear_realm, True, acting_user=support_admin)
|
||||
billing_session = RealmBillingSession(
|
||||
user=support_admin, realm=lear_realm, support_session=True
|
||||
)
|
||||
billing_session.update_customer_sponsorship_status(True)
|
||||
king_user = self.lear_user("king")
|
||||
king_user.role = UserProfile.ROLE_REALM_OWNER
|
||||
king_user.save()
|
||||
|
|
|
@ -55,16 +55,15 @@ if settings.ZILENCER_ENABLED:
|
|||
if settings.BILLING_ENABLED:
|
||||
from corporate.lib.stripe import (
|
||||
RealmBillingSession,
|
||||
SupportType,
|
||||
SupportViewRequest,
|
||||
get_latest_seat_count,
|
||||
void_all_open_invoices,
|
||||
)
|
||||
from corporate.lib.support import (
|
||||
approve_realm_sponsorship,
|
||||
attach_discount_to_realm,
|
||||
get_discount_for_realm,
|
||||
switch_realm_from_standard_to_plus_plan,
|
||||
update_realm_billing_modality,
|
||||
update_realm_sponsorship_status,
|
||||
)
|
||||
from corporate.models import (
|
||||
Customer,
|
||||
|
@ -197,7 +196,21 @@ def support(
|
|||
assert realm_id is not None
|
||||
realm = Realm.objects.get(id=realm_id)
|
||||
|
||||
if plan_type is not None:
|
||||
support_view_request = None
|
||||
|
||||
if approve_sponsorship:
|
||||
support_view_request = SupportViewRequest(support_type=SupportType.approve_sponsorship)
|
||||
elif sponsorship_pending is not None:
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.update_sponsorship_status,
|
||||
sponsorship_status=sponsorship_pending,
|
||||
)
|
||||
elif discount is not None:
|
||||
support_view_request = SupportViewRequest(
|
||||
support_type=SupportType.attach_discount,
|
||||
discount=discount,
|
||||
)
|
||||
elif plan_type is not None:
|
||||
current_plan_type = realm.plan_type
|
||||
do_change_realm_plan_type(realm, plan_type, acting_user=acting_user)
|
||||
msg = f"Plan type of {realm.string_id} changed from {get_plan_name(current_plan_type)} to {get_plan_name(plan_type)} "
|
||||
|
@ -207,12 +220,6 @@ def support(
|
|||
do_change_realm_org_type(realm, org_type, acting_user=acting_user)
|
||||
msg = f"Org type of {realm.string_id} changed from {get_org_type_display_name(current_realm_type)} to {get_org_type_display_name(org_type)} "
|
||||
context["success_message"] = msg
|
||||
elif discount is not None:
|
||||
current_discount = get_discount_for_realm(realm) or 0
|
||||
attach_discount_to_realm(realm, discount, acting_user=acting_user)
|
||||
context[
|
||||
"success_message"
|
||||
] = f"Discount of {realm.string_id} changed to {discount}% from {current_discount}%."
|
||||
elif new_subdomain is not None:
|
||||
old_subdomain = realm.string_id
|
||||
try:
|
||||
|
@ -251,16 +258,6 @@ def support(
|
|||
context[
|
||||
"success_message"
|
||||
] = f"Billing collection method of {realm.string_id} updated to charge automatically."
|
||||
elif sponsorship_pending is not None:
|
||||
if sponsorship_pending:
|
||||
update_realm_sponsorship_status(realm, True, acting_user=acting_user)
|
||||
context["success_message"] = f"{realm.string_id} marked as pending sponsorship."
|
||||
else:
|
||||
update_realm_sponsorship_status(realm, False, acting_user=acting_user)
|
||||
context["success_message"] = f"{realm.string_id} is no longer pending sponsorship."
|
||||
elif approve_sponsorship:
|
||||
approve_realm_sponsorship(realm, acting_user=acting_user)
|
||||
context["success_message"] = f"Sponsorship approved for {realm.string_id}"
|
||||
elif modify_plan is not None:
|
||||
billing_session = RealmBillingSession(
|
||||
user=acting_user, realm=realm, support_session=True
|
||||
|
@ -294,6 +291,13 @@ def support(
|
|||
do_delete_user_preserving_messages(user_profile_for_deletion)
|
||||
context["success_message"] = f"{user_email} in {realm.subdomain} deleted."
|
||||
|
||||
if support_view_request is not None:
|
||||
billing_session = RealmBillingSession(
|
||||
user=acting_user, realm=realm, support_session=True
|
||||
)
|
||||
success_message = billing_session.process_support_view_request(support_view_request)
|
||||
context["success_message"] = success_message
|
||||
|
||||
if query:
|
||||
key_words = get_invitee_emails_set(query)
|
||||
|
||||
|
|
|
@ -522,6 +522,18 @@ class EventStatusRequest:
|
|||
stripe_payment_intent_id: Optional[str]
|
||||
|
||||
|
||||
class SupportType(Enum):
|
||||
approve_sponsorship = 1
|
||||
update_sponsorship_status = 2
|
||||
attach_discount = 3
|
||||
|
||||
|
||||
class SupportViewRequest(TypedDict, total=False):
|
||||
support_type: SupportType
|
||||
sponsorship_status: Optional[bool]
|
||||
discount: Optional[Decimal]
|
||||
|
||||
|
||||
class AuditLogEventType(Enum):
|
||||
STRIPE_CUSTOMER_CREATED = 1
|
||||
STRIPE_CARD_CHANGED = 2
|
||||
|
@ -605,6 +617,11 @@ class SponsorshipRequestForm(forms.Form):
|
|||
|
||||
|
||||
class BillingSession(ABC):
|
||||
@property
|
||||
@abstractmethod
|
||||
def billing_entity_display_name(self) -> str:
|
||||
pass
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def billing_session_url(self) -> str:
|
||||
|
@ -672,7 +689,7 @@ class BillingSession(ABC):
|
|||
pass
|
||||
|
||||
@abstractmethod
|
||||
def approve_sponsorship(self) -> None:
|
||||
def approve_sponsorship(self) -> str:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
|
@ -868,29 +885,32 @@ class BillingSession(ABC):
|
|||
)
|
||||
return stripe_session
|
||||
|
||||
def attach_discount_to_customer(self, discount: Decimal) -> None:
|
||||
def attach_discount_to_customer(self, new_discount: Decimal) -> str:
|
||||
customer = self.get_customer()
|
||||
old_discount: Optional[Decimal] = None
|
||||
old_discount = None
|
||||
if customer is not None:
|
||||
old_discount = customer.default_discount
|
||||
customer.default_discount = discount
|
||||
customer.default_discount = new_discount
|
||||
customer.save(update_fields=["default_discount"])
|
||||
else:
|
||||
customer = self.update_or_create_customer(defaults={"default_discount": discount})
|
||||
customer = self.update_or_create_customer(defaults={"default_discount": new_discount})
|
||||
plan = get_current_plan_by_customer(customer)
|
||||
if plan is not None:
|
||||
plan.price_per_license = get_price_per_license(
|
||||
plan.tier, plan.billing_schedule, discount
|
||||
plan.tier, plan.billing_schedule, new_discount
|
||||
)
|
||||
plan.discount = discount
|
||||
plan.discount = new_discount
|
||||
plan.save(update_fields=["price_per_license", "discount"])
|
||||
self.write_to_audit_log(
|
||||
event_type=AuditLogEventType.DISCOUNT_CHANGED,
|
||||
event_time=timezone_now(),
|
||||
extra_data={"old_discount": old_discount, "new_discount": discount},
|
||||
extra_data={"old_discount": old_discount, "new_discount": new_discount},
|
||||
)
|
||||
if old_discount is None:
|
||||
old_discount = Decimal(0)
|
||||
return f"Discount for {self.billing_entity_display_name} changed to {new_discount}% from {old_discount}%."
|
||||
|
||||
def update_customer_sponsorship_status(self, sponsorship_pending: bool) -> None:
|
||||
def update_customer_sponsorship_status(self, sponsorship_pending: bool) -> str:
|
||||
customer = self.get_customer()
|
||||
if customer is None:
|
||||
customer = self.update_or_create_customer()
|
||||
|
@ -902,6 +922,14 @@ class BillingSession(ABC):
|
|||
extra_data={"sponsorship_pending": sponsorship_pending},
|
||||
)
|
||||
|
||||
if sponsorship_pending:
|
||||
success_message = f"{self.billing_entity_display_name} marked as pending sponsorship."
|
||||
else:
|
||||
success_message = (
|
||||
f"{self.billing_entity_display_name} is no longer pending sponsorship."
|
||||
)
|
||||
return success_message
|
||||
|
||||
def update_billing_modality_of_current_plan(self, charge_automatically: bool) -> None:
|
||||
customer = self.get_customer()
|
||||
if customer is not None:
|
||||
|
@ -1981,6 +2009,23 @@ class BillingSession(ABC):
|
|||
context=context,
|
||||
)
|
||||
|
||||
def process_support_view_request(self, support_request: SupportViewRequest) -> str:
|
||||
support_type = support_request["support_type"]
|
||||
success_message = ""
|
||||
|
||||
if support_type == SupportType.approve_sponsorship:
|
||||
success_message = self.approve_sponsorship()
|
||||
elif support_type == SupportType.update_sponsorship_status:
|
||||
assert support_request["sponsorship_status"] is not None
|
||||
sponsorship_status = support_request["sponsorship_status"]
|
||||
success_message = self.update_customer_sponsorship_status(sponsorship_status)
|
||||
elif support_type == SupportType.attach_discount:
|
||||
assert support_request["discount"] is not None
|
||||
new_discount = support_request["discount"]
|
||||
success_message = self.attach_discount_to_customer(new_discount)
|
||||
|
||||
return success_message
|
||||
|
||||
|
||||
class RealmBillingSession(BillingSession):
|
||||
def __init__(
|
||||
|
@ -2010,6 +2055,11 @@ class RealmBillingSession(BillingSession):
|
|||
Realm.PLAN_TYPE_PLUS,
|
||||
]
|
||||
|
||||
@override
|
||||
@property
|
||||
def billing_entity_display_name(self) -> str:
|
||||
return self.realm.string_id
|
||||
|
||||
@override
|
||||
@property
|
||||
def billing_session_url(self) -> str:
|
||||
|
@ -2179,7 +2229,7 @@ class RealmBillingSession(BillingSession):
|
|||
plan.save(update_fields=["status"])
|
||||
|
||||
@override
|
||||
def approve_sponsorship(self) -> None:
|
||||
def approve_sponsorship(self) -> str:
|
||||
# Sponsorship approval is only a support admin action.
|
||||
assert self.support_session
|
||||
|
||||
|
@ -2210,6 +2260,7 @@ class RealmBillingSession(BillingSession):
|
|||
end_link="](/help/linking-to-zulip-website)",
|
||||
)
|
||||
internal_send_private_message(notification_bot, user, message)
|
||||
return f"Sponsorship approved for {self.billing_entity_display_name}"
|
||||
|
||||
@override
|
||||
def is_sponsored(self) -> bool:
|
||||
|
@ -2322,6 +2373,11 @@ class RemoteRealmBillingSession(BillingSession): # nocoverage
|
|||
else:
|
||||
self.support_session = False
|
||||
|
||||
@override
|
||||
@property
|
||||
def billing_entity_display_name(self) -> str:
|
||||
return self.remote_realm.name
|
||||
|
||||
@override
|
||||
@property
|
||||
def billing_session_url(self) -> str:
|
||||
|
@ -2476,9 +2532,9 @@ class RemoteRealmBillingSession(BillingSession): # nocoverage
|
|||
self.remote_realm.save(update_fields=["plan_type"])
|
||||
|
||||
@override
|
||||
def approve_sponsorship(self) -> None:
|
||||
def approve_sponsorship(self) -> str:
|
||||
# TBD
|
||||
pass
|
||||
return ""
|
||||
|
||||
@override
|
||||
def is_sponsored(self) -> bool:
|
||||
|
@ -2595,6 +2651,11 @@ class RemoteServerBillingSession(BillingSession): # nocoverage
|
|||
else:
|
||||
self.support_session = False
|
||||
|
||||
@override
|
||||
@property
|
||||
def billing_entity_display_name(self) -> str:
|
||||
return self.remote_server.hostname
|
||||
|
||||
@override
|
||||
@property
|
||||
def billing_session_url(self) -> str:
|
||||
|
@ -2739,9 +2800,9 @@ class RemoteServerBillingSession(BillingSession): # nocoverage
|
|||
self.remote_server.save(update_fields=["plan_type"])
|
||||
|
||||
@override
|
||||
def approve_sponsorship(self) -> None:
|
||||
def approve_sponsorship(self) -> str:
|
||||
# TBD
|
||||
pass
|
||||
return ""
|
||||
|
||||
@override
|
||||
def process_downgrade(self, plan: CustomerPlan) -> None:
|
||||
|
|
|
@ -26,23 +26,6 @@ def get_discount_for_realm(realm: Realm) -> Optional[Decimal]:
|
|||
return None
|
||||
|
||||
|
||||
def attach_discount_to_realm(realm: Realm, discount: Decimal, *, acting_user: UserProfile) -> None:
|
||||
billing_session = RealmBillingSession(acting_user, realm, support_session=True)
|
||||
billing_session.attach_discount_to_customer(discount)
|
||||
|
||||
|
||||
def approve_realm_sponsorship(realm: Realm, *, acting_user: UserProfile) -> None:
|
||||
billing_session = RealmBillingSession(acting_user, realm, support_session=True)
|
||||
billing_session.approve_sponsorship()
|
||||
|
||||
|
||||
def update_realm_sponsorship_status(
|
||||
realm: Realm, sponsorship_pending: bool, *, acting_user: UserProfile
|
||||
) -> None:
|
||||
billing_session = RealmBillingSession(acting_user, realm, support_session=True)
|
||||
billing_session.update_customer_sponsorship_status(sponsorship_pending)
|
||||
|
||||
|
||||
def update_realm_billing_modality(
|
||||
realm: Realm, charge_automatically: bool, *, acting_user: UserProfile
|
||||
) -> None:
|
||||
|
|
|
@ -75,12 +75,9 @@ from corporate.lib.stripe import (
|
|||
void_all_open_invoices,
|
||||
)
|
||||
from corporate.lib.support import (
|
||||
approve_realm_sponsorship,
|
||||
attach_discount_to_realm,
|
||||
get_discount_for_realm,
|
||||
switch_realm_from_standard_to_plus_plan,
|
||||
update_realm_billing_modality,
|
||||
update_realm_sponsorship_status,
|
||||
)
|
||||
from corporate.models import (
|
||||
Customer,
|
||||
|
@ -3756,7 +3753,10 @@ class StripeTest(StripeTestCase):
|
|||
users_to_create=1, create_stripe_customer=False, create_plan=False
|
||||
)
|
||||
# To create local Customer object but no Stripe customer.
|
||||
attach_discount_to_realm(realm, Decimal(20), acting_user=self.example_user("iago"))
|
||||
billing_session = RealmBillingSession(
|
||||
user=self.example_user("iago"), realm=realm, support_session=True
|
||||
)
|
||||
billing_session.attach_discount_to_customer(Decimal(20))
|
||||
rows.append(Row(realm, Realm.PLAN_TYPE_SELF_HOSTED, None, None, False, False))
|
||||
|
||||
realm, _, _ = create_realm(
|
||||
|
@ -5108,11 +5108,12 @@ class TestRemoteServerBillingSession(StripeTestCase):
|
|||
|
||||
class TestSupportBillingHelpers(StripeTestCase):
|
||||
def test_get_discount_for_realm(self) -> None:
|
||||
iago = self.example_user("iago")
|
||||
support_admin = self.example_user("iago")
|
||||
user = self.example_user("hamlet")
|
||||
self.assertEqual(get_discount_for_realm(user.realm), None)
|
||||
|
||||
attach_discount_to_realm(user.realm, Decimal(85), acting_user=iago)
|
||||
billing_session = RealmBillingSession(support_admin, realm=user.realm, support_session=True)
|
||||
billing_session.attach_discount_to_customer(Decimal(85))
|
||||
self.assertEqual(get_discount_for_realm(user.realm), 85)
|
||||
|
||||
@mock_stripe()
|
||||
|
@ -5120,7 +5121,8 @@ class TestSupportBillingHelpers(StripeTestCase):
|
|||
# Attach discount before Stripe customer exists
|
||||
support_admin = self.example_user("iago")
|
||||
user = self.example_user("hamlet")
|
||||
attach_discount_to_realm(user.realm, Decimal(85), acting_user=support_admin)
|
||||
billing_session = RealmBillingSession(support_admin, realm=user.realm, support_session=True)
|
||||
billing_session.attach_discount_to_customer(Decimal(85))
|
||||
realm_audit_log = RealmAuditLog.objects.filter(
|
||||
event_type=RealmAuditLog.REALM_DISCOUNT_CHANGED
|
||||
).last()
|
||||
|
@ -5149,7 +5151,8 @@ class TestSupportBillingHelpers(StripeTestCase):
|
|||
# Attach discount to existing Stripe customer
|
||||
plan.status = CustomerPlan.ENDED
|
||||
plan.save(update_fields=["status"])
|
||||
attach_discount_to_realm(user.realm, Decimal(25), acting_user=support_admin)
|
||||
billing_session = RealmBillingSession(support_admin, realm=user.realm, support_session=True)
|
||||
billing_session.attach_discount_to_customer(Decimal(25))
|
||||
with time_machine.travel(self.now, tick=False):
|
||||
self.add_card_and_upgrade(
|
||||
user, license_management="automatic", billing_modality="charge_automatically"
|
||||
|
@ -5165,7 +5168,8 @@ class TestSupportBillingHelpers(StripeTestCase):
|
|||
)
|
||||
plan = CustomerPlan.objects.get(price_per_license=6000, discount=Decimal(25))
|
||||
|
||||
attach_discount_to_realm(user.realm, Decimal(50), acting_user=support_admin)
|
||||
billing_session = RealmBillingSession(support_admin, realm=user.realm, support_session=True)
|
||||
billing_session.attach_discount_to_customer(Decimal(50))
|
||||
plan.refresh_from_db()
|
||||
self.assertEqual(plan.price_per_license, 4000)
|
||||
self.assertEqual(plan.discount, 50)
|
||||
|
@ -5192,7 +5196,8 @@ class TestSupportBillingHelpers(StripeTestCase):
|
|||
self.assertNotEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD_FREE)
|
||||
|
||||
support_admin = self.example_user("iago")
|
||||
approve_realm_sponsorship(realm, acting_user=support_admin)
|
||||
billing_session = RealmBillingSession(user=support_admin, realm=realm, support_session=True)
|
||||
billing_session.approve_sponsorship()
|
||||
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD_FREE)
|
||||
|
||||
expected_message = (
|
||||
|
@ -5222,7 +5227,8 @@ class TestSupportBillingHelpers(StripeTestCase):
|
|||
def test_update_realm_sponsorship_status(self) -> None:
|
||||
lear = get_realm("lear")
|
||||
iago = self.example_user("iago")
|
||||
update_realm_sponsorship_status(lear, True, acting_user=iago)
|
||||
billing_session = RealmBillingSession(user=iago, realm=lear, support_session=True)
|
||||
billing_session.update_customer_sponsorship_status(True)
|
||||
customer = get_customer_by_realm(realm=lear)
|
||||
assert customer is not None
|
||||
self.assertTrue(customer.sponsorship_pending)
|
||||
|
|
Loading…
Reference in New Issue