remote-support: Add ability to configure temporary courtesy plan.

Expands section for scheduling plans in the remote support view to
have a form to create a temporary courtesy plan (aka our legacy
plan for remote servers and realms).

Form is not shown if there is a current plan for the remote billing
entity, and would raise a SupportRequestError in that case as well.
This commit is contained in:
Lauryn Menard 2024-08-15 18:29:51 +02:00 committed by Tim Abbott
parent f27cee21e3
commit 3d58a7ec04
7 changed files with 99 additions and 7 deletions

View File

@ -579,6 +579,7 @@ class SupportType(Enum):
update_required_plan_tier = 8
configure_fixed_price_plan = 9
delete_fixed_price_next_plan = 10
configure_temporary_courtesy_plan = 11
class SupportViewRequest(TypedDict, total=False):
@ -1411,6 +1412,28 @@ class BillingSession(ABC):
plan_tier_name = CustomerPlan.name_from_tier(new_plan_tier)
return f"Required plan tier for {self.billing_entity_display_name} set to {plan_tier_name}."
def configure_temporary_courtesy_plan(self, end_date_string: str) -> str:
plan_end_date = datetime.strptime(end_date_string, "%Y-%m-%d").replace(tzinfo=timezone.utc)
if plan_end_date.date() <= timezone_now().date():
raise SupportRequestError(
f"Cannot configure a courtesy plan for {self.billing_entity_display_name} to end on {end_date_string}."
)
customer = self.get_customer()
if customer is not None:
plan = get_current_plan_by_customer(customer)
if plan is not None:
raise SupportRequestError(
f"Cannot configure a courtesy plan for {self.billing_entity_display_name} because of current plan."
)
plan_anchor_date = timezone_now()
if isinstance(self, RealmBillingSession):
raise SupportRequestError(
f"Cannot currently configure a courtesy plan for {self.billing_entity_display_name}."
) # nocoverage
self.migrate_customer_to_legacy_plan(plan_anchor_date, plan_end_date)
return f"Temporary courtesy plan for {self.billing_entity_display_name} configured to end on {end_date_string}."
def configure_fixed_price_plan(self, fixed_price: int, sent_invoice_id: str | None) -> str:
customer = self.get_customer()
if customer is None:
@ -3384,6 +3407,10 @@ class BillingSession(ABC):
new_fixed_price = support_request["fixed_price"]
sent_invoice_id = support_request["sent_invoice_id"]
success_message = self.configure_fixed_price_plan(new_fixed_price, sent_invoice_id)
elif support_type == SupportType.configure_temporary_courtesy_plan:
assert support_request["plan_end_date"] is not None
temporary_plan_end_date = support_request["plan_end_date"]
success_message = self.configure_temporary_courtesy_plan(temporary_plan_end_date)
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

View File

@ -7352,7 +7352,9 @@ class TestRemoteRealmBillingFlow(StripeTestCase, RemoteRealmBillingTestCase):
self.assertEqual(success_message, "Fixed price offer deleted")
result = self.client_get("/activity/remote/support", {"q": "example.com"})
self.assert_not_in_success_response(["Next plan information:"], result)
self.assert_in_success_response(["Fixed price", "Annual amount in dollars"], result)
self.assert_in_success_response(
["Configure fixed price plan", "Annual amount in dollars"], result
)
@responses.activate
@mock_stripe()

View File

@ -503,6 +503,50 @@ class TestRemoteServerSupportEndpoint(ZulipTestCase):
["Cannot update current plan for realm-name-5 to end on 2020-01-01."], result
)
def test_configure_temporary_courtesy_plan(self) -> None:
iago = self.example_user("iago")
self.login_user(iago)
remote_realm = RemoteRealm.objects.get(name="realm-name-4")
# Cannot configure courtesy plan to end in the past.
result = self.client_post(
"/activity/remote/support",
{
"remote_realm_id": f"{remote_realm.id}",
"temporary_courtesy_plan": "2010-03-01",
},
)
self.assert_in_success_response(
["Cannot configure a courtesy plan for realm-name-4 to end on 2010-03-01."],
result,
)
# Cannot configure courtesy plan if there is a current plan for billing entity.
result = self.client_post(
"/activity/remote/support",
{
"remote_realm_id": f"{remote_realm.id}",
"temporary_courtesy_plan": "2050-03-01",
},
)
self.assert_in_success_response(
["Cannot configure a courtesy plan for realm-name-4 because of current plan."],
result,
)
remote_realm = RemoteRealm.objects.get(name="realm-name-2")
assert remote_realm.plan_type == RemoteRealm.PLAN_TYPE_SELF_MANAGED
result = self.client_post(
"/activity/remote/support",
{
"remote_realm_id": f"{remote_realm.id}",
"temporary_courtesy_plan": "2050-03-01",
},
)
self.assert_in_success_response(
["Temporary courtesy plan for realm-name-2 configured to end on 2050-03-01."],
result,
)
remote_realm.refresh_from_db()
assert remote_realm.plan_type == RemoteRealm.PLAN_TYPE_SELF_MANAGED_LEGACY
def test_discount_support_actions_when_upgrade_scheduled(self) -> None:
remote_realm = RemoteRealm.objects.get(name="realm-name-4")
billing_session = RemoteRealmBillingSession(remote_realm=remote_realm)

View File

@ -651,6 +651,10 @@ def remote_servers_support(
modify_plan: VALID_MODIFY_PLAN_METHODS | None = None,
delete_fixed_price_next_plan: Json[bool] = False,
remote_server_status: VALID_STATUS_VALUES | None = None,
temporary_courtesy_plan: Annotated[
str, AfterValidator(lambda x: check_date("temporary_courtesy_plan", x))
]
| None = None,
) -> HttpResponse:
context: dict[str, Any] = {}
@ -706,6 +710,11 @@ def remote_servers_support(
fixed_price=fixed_price,
sent_invoice_id=sent_invoice_id,
)
elif temporary_courtesy_plan is not None:
support_view_request = SupportViewRequest(
support_type=SupportType.configure_temporary_courtesy_plan,
plan_end_date=temporary_courtesy_plan,
)
elif billing_modality is not None:
support_view_request = SupportViewRequest(
support_type=SupportType.update_billing_modality,

View File

@ -1,14 +1,24 @@
<p class="support-section-header">⏱️ Schedule fixed price plan:</p>
<p class="support-section-header">⏱️ Schedule plan:</p>
<form method="POST" class="remote-form">
<b>Fixed price</b><br />
{% if not is_current_plan_billable %}
<b>Configure fixed price plan:</b><br />
{% if not plan_data.is_current_plan_billable %}
<i>Enter Invoice ID only if the customer chose to pay by invoice.</i><br />
{% endif %}
{{ csrf_input }}
<input type="hidden" name="{{ remote_type }}" value="{{ remote_id }}" />
<input type="number" name="fixed_price" placeholder="Annual amount in dollars" required />
{% if not is_current_plan_billable %}
{% if not plan_data.is_current_plan_billable %}
<input type="text" name="sent_invoice_id" placeholder="Sent invoice ID" />
{% endif %}
<button type="submit" class="support-submit-button">Schedule</button>
</form>
{% if not plan_data.current_plan %}
<form method="POST" class="remote-form">
<b>Configure temporary courtesy plan:</b><br />
<i>Once created, the end date for the temporary courtesy plan can be extended.</i><br />
{{ csrf_input }}
<input type="hidden" name="{{ remote_type }}" value="{{ remote_id }}" />
<input type="date" name="temporary_courtesy_plan" required />
<button type="submit" class="support-submit-button">Create</button>
</form>
{% endif %}

View File

@ -94,7 +94,7 @@
{% elif not remote_server_deactivated %}
<div class="next-plan-container">
{% with %}
{% set is_current_plan_billable = support_data[remote_realm.id].plan_data.is_current_plan_billable %}
{% set plan_data = support_data[remote_realm.id].plan_data %}
{% set remote_id = remote_realm.id %}
{% set remote_type = "remote_realm_id" %}
{% include 'corporate/support/next_plan_forms_support.html' %}

View File

@ -139,7 +139,7 @@
{% elif not remote_server.deactivated %}
<div class="next-plan-container">
{% with %}
{% set is_current_plan_billable = remote_servers_support_data[remote_server.id].plan_data.is_current_plan_billable %}
{% set plan_data = remote_servers_support_data[remote_server.id].plan_data %}
{% set remote_id = remote_server.id %}
{% set remote_type = "remote_server_id" %}
{% include 'corporate/support/next_plan_forms_support.html' %}