From 76d9aff5a62f40f48f676638af5f99bed211373c Mon Sep 17 00:00:00 2001 From: Aman Agrawal Date: Fri, 8 Dec 2023 07:25:05 +0000 Subject: [PATCH] sponsorship: Allow remote orgs to request a plan type. Sponsorship and billing pages modified to reflect the correctly requested sponsorship plan name. Add a line break before "Contact Zulip support". --- corporate/lib/stripe.py | 44 +++++++++++++++---- ..._zulipsponsorshiprequest_requested_plan.py | 21 +++++++++ corporate/models.py | 14 ++++++ corporate/tests/test_stripe.py | 4 +- templates/corporate/billing.html | 6 ++- templates/corporate/sponsorship.html | 23 +++++++++- .../zerver/emails/sponsorship_request.html | 5 +++ 7 files changed, 105 insertions(+), 12 deletions(-) create mode 100644 corporate/migrations/0028_zulipsponsorshiprequest_requested_plan.py diff --git a/corporate/lib/stripe.py b/corporate/lib/stripe.py index a5a504d6c3..d8d227a456 100644 --- a/corporate/lib/stripe.py +++ b/corporate/lib/stripe.py @@ -30,6 +30,7 @@ from corporate.models import ( LicenseLedger, PaymentIntent, Session, + SponsoredPlanTypes, ZulipSponsorshipRequest, get_current_plan_by_customer, get_current_plan_by_realm, @@ -645,6 +646,9 @@ class SponsorshipRequestForm(forms.Form): expected_total_users = forms.CharField(widget=forms.Textarea) paid_users_count = forms.CharField(widget=forms.Textarea) paid_users_description = forms.CharField(widget=forms.Textarea, required=False) + requested_plan = forms.ChoiceField( + choices=[(plan.value, plan.name) for plan in SponsoredPlanTypes], required=False + ) class BillingSession(ABC): @@ -1654,6 +1658,7 @@ class BillingSession(ABC): customer, status=CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END ) legacy_remote_server_new_plan_name = self.get_legacy_remote_server_new_plan_name(customer) + is_self_hosted_billing = not isinstance(self, RealmBillingSession) context = { "plan_name": plan.name, "has_active_plan": True, @@ -1676,8 +1681,11 @@ class BillingSession(ABC): "fixed_price": fixed_price, "price_per_license": price_per_license, "is_sponsorship_pending": customer.sponsorship_pending, + "sponsorship_plan_name": self.get_sponsorship_plan_name( + customer, is_self_hosted_billing + ), "discount_percent": format_discount_percentage(customer.default_discount), - "is_self_hosted_billing": not isinstance(self, RealmBillingSession), + "is_self_hosted_billing": is_self_hosted_billing, "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, @@ -2218,11 +2226,22 @@ class BillingSession(ABC): raise JsonableError(_("Pass stripe_session_id or stripe_payment_intent_id")) - def get_sponsorship_request_context(self) -> Optional[Dict[str, Any]]: - customer = self.get_customer() - is_remotely_hosted = isinstance( - self, (RemoteRealmBillingSession, RemoteServerBillingSession) - ) + def get_sponsorship_plan_name( + self, customer: Optional[Customer], is_remotely_hosted: bool + ) -> str: + if customer is not None and customer.sponsorship_pending: + # For sponsorship pending requests, we also show the type of sponsorship requested. + # In other cases, we just show the plan user is currently on. + sponsorship_request = ( + ZulipSponsorshipRequest.objects.filter(customer=customer).order_by("-id").first() + ) + # It's possible that we marked `customer.sponsorship_pending` via support page + # without user submitting a sponsorship request. + if sponsorship_request is not None and sponsorship_request.requested_plan not in ( + None, + SponsoredPlanTypes.UNSPECIFIED.value, + ): # nocoverage + return sponsorship_request.requested_plan # We only support sponsorships for these plans. sponsored_plan_name = CustomerPlan.name_from_tier(CustomerPlan.TIER_CLOUD_STANDARD) @@ -2231,6 +2250,14 @@ class BillingSession(ABC): CustomerPlan.TIER_SELF_HOSTED_COMMUNITY ) + return sponsored_plan_name + + def get_sponsorship_request_context(self) -> Optional[Dict[str, Any]]: + customer = self.get_customer() + is_remotely_hosted = isinstance( + self, (RemoteRealmBillingSession, RemoteServerBillingSession) + ) + plan_name = "Zulip Cloud Free" if is_remotely_hosted: plan_name = "Self-managed" @@ -2238,7 +2265,7 @@ class BillingSession(ABC): context: Dict[str, Any] = { "billing_base_url": self.billing_base_url, "is_remotely_hosted": is_remotely_hosted, - "sponsored_plan_name": sponsored_plan_name, + "sponsorship_plan_name": self.get_sponsorship_plan_name(customer, is_remotely_hosted), "plan_name": plan_name, } @@ -2250,7 +2277,6 @@ class BillingSession(ABC): if self.is_sponsored(): context["is_sponsored"] = True - context["plan_name"] = sponsored_plan_name if customer is not None: plan = get_current_plan_by_customer(customer) @@ -2283,6 +2309,7 @@ class BillingSession(ABC): expected_total_users=form.cleaned_data["expected_total_users"], paid_users_count=form.cleaned_data["paid_users_count"], paid_users_description=form.cleaned_data["paid_users_description"], + requested_plan=form.cleaned_data["requested_plan"], ) sponsorship_request.save() @@ -2310,6 +2337,7 @@ class BillingSession(ABC): "expected_total_users": sponsorship_request.expected_total_users, "paid_users_count": sponsorship_request.paid_users_count, "paid_users_description": sponsorship_request.paid_users_description, + "requested_plan": sponsorship_request.requested_plan, } send_email( "zerver/emails/sponsorship_request", diff --git a/corporate/migrations/0028_zulipsponsorshiprequest_requested_plan.py b/corporate/migrations/0028_zulipsponsorshiprequest_requested_plan.py new file mode 100644 index 0000000000..a0c6fd68b2 --- /dev/null +++ b/corporate/migrations/0028_zulipsponsorshiprequest_requested_plan.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.8 on 2023-12-08 18:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("corporate", "0027_alter_zulipsponsorshiprequest_requested_by"), + ] + + operations = [ + migrations.AddField( + model_name="zulipsponsorshiprequest", + name="requested_plan", + field=models.CharField( + choices=[("", "UNSPECIFIED"), ("Community", "COMMUNITY"), ("Business", "BUSINESS")], + default="", + max_length=50, + ), + ), + ] diff --git a/corporate/models.py b/corporate/models.py index 7a11f0cc56..6d3483ef54 100644 --- a/corporate/models.py +++ b/corporate/models.py @@ -1,3 +1,4 @@ +from enum import Enum from typing import Any, Dict, Optional, Union from django.contrib.contenttypes.fields import GenericForeignKey @@ -384,6 +385,13 @@ class LicenseLedger(models.Model): licenses_at_next_renewal = models.IntegerField(null=True) +class SponsoredPlanTypes(Enum): + # unspecified used for cloud sponsorship requests + UNSPECIFIED = "" + COMMUNITY = "Community" + BUSINESS = "Business" + + class ZulipSponsorshipRequest(models.Model): customer = models.ForeignKey(Customer, on_delete=CASCADE) requested_by = models.ForeignKey(UserProfile, on_delete=CASCADE, null=True, blank=True) @@ -400,3 +408,9 @@ class ZulipSponsorshipRequest(models.Model): expected_total_users = models.TextField(default="") paid_users_count = models.TextField(default="") paid_users_description = models.TextField(default="") + + requested_plan = models.CharField( + max_length=50, + choices=[(plan.value, plan.name) for plan in SponsoredPlanTypes], + default=SponsoredPlanTypes.UNSPECIFIED.value, + ) diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index 9935410cfb..aa9832a431 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -1865,7 +1865,9 @@ class StripeTest(StripeTestCase): response = self.client_get("/sponsorship/") self.assert_in_success_response( [ - 'This organization has requested sponsorship for a Zulip Cloud Standard plan. Contact Zulip support with any questions or updates.' + "This organization has requested sponsorship for a", + 'Zulip Cloud Standard', + 'plan.
Contact Zulip support with any questions or updates.', ], response, ) diff --git a/templates/corporate/billing.html b/templates/corporate/billing.html index ea2447f6e6..e78dbe7c9a 100644 --- a/templates/corporate/billing.html +++ b/templates/corporate/billing.html @@ -11,7 +11,11 @@ {% if admin_access and has_active_plan %} {% if is_sponsorship_pending %}
- This organization has requested sponsorship for a {{ plan_name }} plan. Contact Zulip support with any questions or updates. + This organization has requested sponsorship for a + {% if sponsorship_plan_name == "Business" %} + discounted + {% endif %} + {{ sponsorship_plan_name }} plan.
Contact Zulip support with any questions or updates.
{% endif %} {% if success_message %} diff --git a/templates/corporate/sponsorship.html b/templates/corporate/sponsorship.html index 3f1b68a944..c1ea5f87a8 100644 --- a/templates/corporate/sponsorship.html +++ b/templates/corporate/sponsorship.html @@ -28,9 +28,13 @@
{% if is_sponsored %} - Zulip is sponsoring a free {{ sponsored_plan_name }} plan for this organization. 🎉 + Zulip is sponsoring a free {{ sponsorship_plan_name }} plan for this organization. 🎉 {% else %} - This organization has requested sponsorship for a {{ sponsored_plan_name }} plan. Contact Zulip support with any questions or updates. + This organization has requested sponsorship for a + {% if sponsorship_plan_name == "Business" %} + discounted + {% endif %} + {{ sponsorship_plan_name }} plan.
Contact Zulip support with any questions or updates. {% endif %}
@@ -107,6 +111,21 @@

+ {% if is_remotely_hosted %} +
+
+ +
+ +
+ {% endif %}
diff --git a/templates/zerver/emails/sponsorship_request.html b/templates/zerver/emails/sponsorship_request.html index 1409f598ee..3e7e403814 100644 --- a/templates/zerver/emails/sponsorship_request.html +++ b/templates/zerver/emails/sponsorship_request.html @@ -34,4 +34,9 @@ Requested by: {{ requested_by }} ({{ user_role }}) +{% if requested_plan %} +

+Requested plan: {{ requested_plan }} +{% endif %} + {% endblock %}