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:
Lauryn Menard 2023-11-30 21:11:54 +01:00 committed by Tim Abbott
parent 0679bc044a
commit 5d25cab42b
5 changed files with 130 additions and 72 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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:

View File

@ -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:

View File

@ -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)