mirror of https://github.com/zulip/zulip.git
billing: Add option to request a sponsorship in /upgrade.
This commit is contained in:
parent
606c2acefe
commit
4c6350fa4b
|
@ -10,6 +10,7 @@ from analytics.lib.counts import COUNT_STATS, CountStat
|
||||||
from analytics.lib.time_utils import time_range
|
from analytics.lib.time_utils import time_range
|
||||||
from analytics.models import FillState, RealmCount, UserCount, last_successful_fill
|
from analytics.models import FillState, RealmCount, UserCount, last_successful_fill
|
||||||
from analytics.views import rewrite_client_arrays, sort_by_totals, sort_client_labels
|
from analytics.views import rewrite_client_arrays, sort_by_totals, sort_client_labels
|
||||||
|
from corporate.models import get_customer_by_realm
|
||||||
from zerver.lib.actions import do_create_multiuse_invite_link, do_send_realm_reactivation_email
|
from zerver.lib.actions import do_create_multiuse_invite_link, do_send_realm_reactivation_email
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.lib.test_helpers import reset_emails_in_zulip_realm
|
from zerver.lib.test_helpers import reset_emails_in_zulip_realm
|
||||||
|
@ -575,6 +576,35 @@ class TestSupportEndpoint(ZulipTestCase):
|
||||||
m.assert_called_once_with(get_realm("lear"), 25)
|
m.assert_called_once_with(get_realm("lear"), 25)
|
||||||
self.assert_in_success_response(["Discount of Lear & Co. changed to 25 from None"], result)
|
self.assert_in_success_response(["Discount of Lear & Co. changed to 25 from None"], result)
|
||||||
|
|
||||||
|
def test_change_sponsorship_status(self) -> None:
|
||||||
|
lear_realm = get_realm("lear")
|
||||||
|
self.assertIsNone(get_customer_by_realm(lear_realm))
|
||||||
|
|
||||||
|
cordelia = self.example_user('cordelia')
|
||||||
|
self.login_user(cordelia)
|
||||||
|
|
||||||
|
result = self.client_post("/activity/support", {"realm_id": f"{lear_realm.id}",
|
||||||
|
"sponsorship_pending": "true"})
|
||||||
|
self.assertEqual(result.status_code, 302)
|
||||||
|
self.assertEqual(result["Location"], "/login/")
|
||||||
|
|
||||||
|
iago = self.example_user("iago")
|
||||||
|
self.login_user(iago)
|
||||||
|
|
||||||
|
result = self.client_post("/activity/support", {"realm_id": f"{lear_realm.id}",
|
||||||
|
"sponsorship_pending": "true"})
|
||||||
|
self.assert_in_success_response(["Lear & Co. marked as pending sponsorship."], result)
|
||||||
|
customer = get_customer_by_realm(lear_realm)
|
||||||
|
assert(customer is not None)
|
||||||
|
self.assertTrue(customer.sponsorship_pending)
|
||||||
|
|
||||||
|
result = self.client_post("/activity/support", {"realm_id": f"{lear_realm.id}",
|
||||||
|
"sponsorship_pending": "false"})
|
||||||
|
self.assert_in_success_response(["Lear & Co. is no longer pending sponsorship."], result)
|
||||||
|
customer = get_customer_by_realm(lear_realm)
|
||||||
|
assert(customer is not None)
|
||||||
|
self.assertFalse(customer.sponsorship_pending)
|
||||||
|
|
||||||
def test_activate_or_deactivate_realm(self) -> None:
|
def test_activate_or_deactivate_realm(self) -> None:
|
||||||
cordelia = self.example_user('cordelia')
|
cordelia = self.example_user('cordelia')
|
||||||
lear_realm = get_realm('lear')
|
lear_realm = get_realm('lear')
|
||||||
|
|
|
@ -70,7 +70,12 @@ from zerver.models import (
|
||||||
from zerver.views.invite import get_invitee_emails_set
|
from zerver.views.invite import get_invitee_emails_set
|
||||||
|
|
||||||
if settings.BILLING_ENABLED:
|
if settings.BILLING_ENABLED:
|
||||||
from corporate.lib.stripe import attach_discount_to_realm, get_discount_for_realm
|
from corporate.lib.stripe import (
|
||||||
|
attach_discount_to_realm,
|
||||||
|
get_customer_by_realm,
|
||||||
|
get_discount_for_realm,
|
||||||
|
update_sponsorship_status,
|
||||||
|
)
|
||||||
|
|
||||||
if settings.ZILENCER_ENABLED:
|
if settings.ZILENCER_ENABLED:
|
||||||
from zilencer.models import RemoteInstallationCount, RemoteRealmCount, RemoteZulipServer
|
from zilencer.models import RemoteInstallationCount, RemoteRealmCount, RemoteZulipServer
|
||||||
|
@ -1136,6 +1141,15 @@ def support(request: HttpRequest) -> HttpResponse:
|
||||||
do_deactivate_realm(realm, request.user)
|
do_deactivate_realm(realm, request.user)
|
||||||
context["message"] = f"{realm.name} deactivated."
|
context["message"] = f"{realm.name} deactivated."
|
||||||
|
|
||||||
|
sponsorship_pending = request.POST.get("sponsorship_pending", None)
|
||||||
|
if sponsorship_pending is not None:
|
||||||
|
if sponsorship_pending == "true":
|
||||||
|
update_sponsorship_status(realm, True)
|
||||||
|
context["message"] = f"{realm.name} marked as pending sponsorship."
|
||||||
|
elif sponsorship_pending == "false":
|
||||||
|
update_sponsorship_status(realm, False)
|
||||||
|
context["message"] = f"{realm.name} is no longer pending sponsorship."
|
||||||
|
|
||||||
scrub_realm = request.POST.get("scrub_realm", None)
|
scrub_realm = request.POST.get("scrub_realm", None)
|
||||||
if scrub_realm is not None:
|
if scrub_realm is not None:
|
||||||
if scrub_realm == "scrub_realm":
|
if scrub_realm == "scrub_realm":
|
||||||
|
@ -1165,6 +1179,9 @@ def support(request: HttpRequest) -> HttpResponse:
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
for realm in realms:
|
||||||
|
realm.customer = get_customer_by_realm(realm)
|
||||||
|
|
||||||
context["realms"] = realms
|
context["realms"] = realms
|
||||||
|
|
||||||
confirmations: List[Dict[str, Any]] = []
|
confirmations: List[Dict[str, Any]] = []
|
||||||
|
|
|
@ -558,6 +558,11 @@ def invoice_plans_as_needed(event_time: datetime=timezone_now()) -> None:
|
||||||
def attach_discount_to_realm(realm: Realm, discount: Decimal) -> None:
|
def attach_discount_to_realm(realm: Realm, discount: Decimal) -> None:
|
||||||
Customer.objects.update_or_create(realm=realm, defaults={'default_discount': discount})
|
Customer.objects.update_or_create(realm=realm, defaults={'default_discount': discount})
|
||||||
|
|
||||||
|
def update_sponsorship_status(realm: Realm, sponsorship_pending: bool) -> None:
|
||||||
|
customer, _ = Customer.objects.get_or_create(realm=realm)
|
||||||
|
customer.sponsorship_pending = sponsorship_pending
|
||||||
|
customer.save(update_fields=["sponsorship_pending"])
|
||||||
|
|
||||||
def get_discount_for_realm(realm: Realm) -> Optional[Decimal]:
|
def get_discount_for_realm(realm: Realm) -> Optional[Decimal]:
|
||||||
customer = get_customer_by_realm(realm)
|
customer = get_customer_by_realm(realm)
|
||||||
if customer is not None:
|
if customer is not None:
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 2.2.13 on 2020-06-09 12:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('corporate', '0008_nullable_next_invoice_date'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customer',
|
||||||
|
name='sponsorship_pending',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
|
@ -11,6 +11,7 @@ from zerver.models import Realm
|
||||||
class Customer(models.Model):
|
class Customer(models.Model):
|
||||||
realm: Realm = models.OneToOneField(Realm, on_delete=CASCADE)
|
realm: Realm = models.OneToOneField(Realm, on_delete=CASCADE)
|
||||||
stripe_customer_id: str = models.CharField(max_length=255, null=True, unique=True)
|
stripe_customer_id: str = models.CharField(max_length=255, null=True, unique=True)
|
||||||
|
sponsorship_pending: bool = models.BooleanField(default=False)
|
||||||
# A percentage, like 85.
|
# A percentage, like 85.
|
||||||
default_discount: Optional[Decimal] = models.DecimalField(decimal_places=4, max_digits=7, null=True)
|
default_discount: Optional[Decimal] = models.DecimalField(decimal_places=4, max_digits=7, null=True)
|
||||||
|
|
||||||
|
|
|
@ -1068,6 +1068,48 @@ class StripeTest(StripeTestCase):
|
||||||
self.assert_json_error_contains(response, "Something went wrong. Please contact desdemona+admin@zulip.com.")
|
self.assert_json_error_contains(response, "Something went wrong. Please contact desdemona+admin@zulip.com.")
|
||||||
self.assertEqual(ujson.loads(response.content)['error_description'], 'uncaught exception during upgrade')
|
self.assertEqual(ujson.loads(response.content)['error_description'], 'uncaught exception during upgrade')
|
||||||
|
|
||||||
|
def test_request_sponsorship(self) -> None:
|
||||||
|
user = self.example_user("hamlet")
|
||||||
|
self.assertIsNone(get_customer_by_realm(user.realm))
|
||||||
|
|
||||||
|
self.login_user(user)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"organization-type": ujson.dumps("Open-source"),
|
||||||
|
"website": ujson.dumps("https://infinispan.org/"),
|
||||||
|
"description": ujson.dumps("Infinispan is a distributed in-memory key/value data store with optional schema."),
|
||||||
|
}
|
||||||
|
response = self.client_post("/json/billing/sponsorship", data)
|
||||||
|
self.assert_json_success(response)
|
||||||
|
|
||||||
|
customer = get_customer_by_realm(user.realm)
|
||||||
|
assert(customer is not None)
|
||||||
|
self.assertEqual(customer.sponsorship_pending, True)
|
||||||
|
from django.core.mail import outbox
|
||||||
|
self.assertEqual(len(outbox), 1)
|
||||||
|
|
||||||
|
for message in outbox:
|
||||||
|
self.assertEqual(len(message.to), 1)
|
||||||
|
self.assertEqual(message.to[0], "desdemona+admin@zulip.com")
|
||||||
|
self.assertEqual(message.subject, "Sponsorship request (Open-source) for zulip")
|
||||||
|
self.assertEqual(message.from_email, f'{user.full_name} <{user.delivery_email}>')
|
||||||
|
self.assertIn("User role: Member", message.body)
|
||||||
|
self.assertIn("Support URL: http://zulip.testserver/activity/support?q=zulip", message.body)
|
||||||
|
self.assertIn("Website: https://infinispan.org", message.body)
|
||||||
|
self.assertIn("Organization type: Open-source", message.body)
|
||||||
|
self.assertIn("Description:\nInfinispan is a distributed in-memory", message.body)
|
||||||
|
|
||||||
|
response = self.client_get("/upgrade/")
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/billing/")
|
||||||
|
|
||||||
|
response = self.client_get("/billing/")
|
||||||
|
self.assert_in_success_response(["Your organization has requested sponsored or discounted hosting."], response)
|
||||||
|
|
||||||
|
self.login_user(self.example_user("othello"))
|
||||||
|
response = self.client_get("/billing/")
|
||||||
|
self.assert_in_success_response(["You must be an organization administrator or a billing administrator to view this page."], response)
|
||||||
|
|
||||||
def test_redirect_for_billing_home(self) -> None:
|
def test_redirect_for_billing_home(self) -> None:
|
||||||
user = self.example_user("iago")
|
user = self.example_user("iago")
|
||||||
self.login_user(user)
|
self.login_user(user)
|
||||||
|
@ -1796,8 +1838,9 @@ class RequiresBillingAccessTest(ZulipTestCase):
|
||||||
string_with_all_endpoints = str(get_resolver('corporate.urls').reverse_dict)
|
string_with_all_endpoints = str(get_resolver('corporate.urls').reverse_dict)
|
||||||
json_endpoints = {word.strip("\"'()[],$") for word in string_with_all_endpoints.split()
|
json_endpoints = {word.strip("\"'()[],$") for word in string_with_all_endpoints.split()
|
||||||
if 'json/' in word}
|
if 'json/' in word}
|
||||||
# No need to test upgrade endpoint as it only requires user to be logged in.
|
# No need to test upgrade and sponsorship endpoints as they only require user to be logged in.
|
||||||
json_endpoints.remove("json/billing/upgrade")
|
json_endpoints.remove("json/billing/upgrade")
|
||||||
|
json_endpoints.remove("json/billing/sponsorship")
|
||||||
|
|
||||||
self.assertEqual(len(json_endpoints), len(params))
|
self.assertEqual(len(json_endpoints), len(params))
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,8 @@ i18n_urlpatterns: Any = [
|
||||||
v1_api_and_json_patterns = [
|
v1_api_and_json_patterns = [
|
||||||
path('billing/upgrade', rest_dispatch,
|
path('billing/upgrade', rest_dispatch,
|
||||||
{'POST': 'corporate.views.upgrade'}),
|
{'POST': 'corporate.views.upgrade'}),
|
||||||
|
path('billing/sponsorship', rest_dispatch,
|
||||||
|
{'POST': 'corporate.views.sponsorship'}),
|
||||||
path('billing/plan/change', rest_dispatch,
|
path('billing/plan/change', rest_dispatch,
|
||||||
{'POST': 'corporate.views.change_plan_status'}),
|
{'POST': 'corporate.views.change_plan_status'}),
|
||||||
path('billing/sources/change', rest_dispatch,
|
path('billing/sources/change', rest_dispatch,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any, Dict, Optional, Union
|
from typing import Any, Dict, Optional, Union
|
||||||
|
from urllib.parse import urlencode, urljoin, urlunsplit
|
||||||
|
|
||||||
import stripe
|
import stripe
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -28,6 +29,7 @@ from corporate.lib.stripe import (
|
||||||
start_of_next_billing_cycle,
|
start_of_next_billing_cycle,
|
||||||
stripe_get_customer,
|
stripe_get_customer,
|
||||||
unsign_string,
|
unsign_string,
|
||||||
|
update_sponsorship_status,
|
||||||
)
|
)
|
||||||
from corporate.models import (
|
from corporate.models import (
|
||||||
CustomerPlan,
|
CustomerPlan,
|
||||||
|
@ -38,8 +40,9 @@ from corporate.models import (
|
||||||
from zerver.decorator import require_billing_access, zulip_login_required
|
from zerver.decorator import require_billing_access, zulip_login_required
|
||||||
from zerver.lib.request import REQ, has_request_variables
|
from zerver.lib.request import REQ, has_request_variables
|
||||||
from zerver.lib.response import json_error, json_success
|
from zerver.lib.response import json_error, json_success
|
||||||
|
from zerver.lib.send_email import FromAddress, send_email
|
||||||
from zerver.lib.validator import check_int, check_string
|
from zerver.lib.validator import check_int, check_string
|
||||||
from zerver.models import UserProfile
|
from zerver.models import UserProfile, get_realm
|
||||||
|
|
||||||
billing_logger = logging.getLogger('corporate.stripe')
|
billing_logger = logging.getLogger('corporate.stripe')
|
||||||
|
|
||||||
|
@ -145,8 +148,9 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
||||||
return render(request, "404.html")
|
return render(request, "404.html")
|
||||||
|
|
||||||
user = request.user
|
user = request.user
|
||||||
|
|
||||||
customer = get_customer_by_realm(user.realm)
|
customer = get_customer_by_realm(user.realm)
|
||||||
if customer is not None and get_current_plan_by_customer(customer) is not None:
|
if customer is not None and (get_current_plan_by_customer(customer) is not None or customer.sponsorship_pending):
|
||||||
billing_page_url = reverse('corporate.views.billing_home')
|
billing_page_url = reverse('corporate.views.billing_home')
|
||||||
if request.GET.get("onboarding") is not None:
|
if request.GET.get("onboarding") is not None:
|
||||||
billing_page_url = f"{billing_page_url}?onboarding=true"
|
billing_page_url = f"{billing_page_url}?onboarding=true"
|
||||||
|
@ -159,6 +163,7 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
||||||
seat_count = get_latest_seat_count(user.realm)
|
seat_count = get_latest_seat_count(user.realm)
|
||||||
signed_seat_count, salt = sign_string(str(seat_count))
|
signed_seat_count, salt = sign_string(str(seat_count))
|
||||||
context: Dict[str, Any] = {
|
context: Dict[str, Any] = {
|
||||||
|
'realm': user.realm,
|
||||||
'publishable_key': STRIPE_PUBLISHABLE_KEY,
|
'publishable_key': STRIPE_PUBLISHABLE_KEY,
|
||||||
'email': user.delivery_email,
|
'email': user.delivery_email,
|
||||||
'seat_count': seat_count,
|
'seat_count': seat_count,
|
||||||
|
@ -179,17 +184,71 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
||||||
response = render(request, 'corporate/upgrade.html', context=context)
|
response = render(request, 'corporate/upgrade.html', context=context)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@has_request_variables
|
||||||
|
def sponsorship(request: HttpRequest, user: UserProfile,
|
||||||
|
organization_type: str=REQ("organization-type", validator=check_string),
|
||||||
|
website: str=REQ("website", validator=check_string),
|
||||||
|
description: str=REQ("description", validator=check_string)) -> HttpResponse:
|
||||||
|
realm = user.realm
|
||||||
|
|
||||||
|
requested_by = user.full_name
|
||||||
|
|
||||||
|
role_id_to_name_map = {
|
||||||
|
UserProfile.ROLE_REALM_OWNER: "Realm owner",
|
||||||
|
UserProfile.ROLE_REALM_ADMINISTRATOR: "Realm adminstrator",
|
||||||
|
UserProfile.ROLE_MEMBER: "Member",
|
||||||
|
UserProfile.ROLE_GUEST: "Guest"
|
||||||
|
}
|
||||||
|
user_role = role_id_to_name_map[user.role]
|
||||||
|
|
||||||
|
support_realm_uri = get_realm(settings.STAFF_SUBDOMAIN).uri
|
||||||
|
support_url = urljoin(support_realm_uri, urlunsplit(("", "", reverse('analytics.views.support'),
|
||||||
|
urlencode({"q": realm.string_id}), "")))
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"requested_by": requested_by,
|
||||||
|
"user_role": user_role,
|
||||||
|
"string_id": realm.string_id,
|
||||||
|
"support_url": support_url,
|
||||||
|
"organization_type": organization_type,
|
||||||
|
"website": website,
|
||||||
|
"description": description,
|
||||||
|
}
|
||||||
|
send_email(
|
||||||
|
"zerver/emails/sponsorship_request",
|
||||||
|
to_emails=[FromAddress.SUPPORT],
|
||||||
|
from_name=user.full_name,
|
||||||
|
from_address=user.delivery_email,
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
update_sponsorship_status(realm, True)
|
||||||
|
user.is_billing_admin = True
|
||||||
|
user.save(update_fields=["is_billing_admin"])
|
||||||
|
|
||||||
|
return json_success()
|
||||||
|
|
||||||
@zulip_login_required
|
@zulip_login_required
|
||||||
def billing_home(request: HttpRequest) -> HttpResponse:
|
def billing_home(request: HttpRequest) -> HttpResponse:
|
||||||
user = request.user
|
user = request.user
|
||||||
customer = get_customer_by_realm(user.realm)
|
customer = get_customer_by_realm(user.realm)
|
||||||
|
context: Dict[str, Any] = {}
|
||||||
|
|
||||||
if customer is None:
|
if customer is None:
|
||||||
return HttpResponseRedirect(reverse('corporate.views.initial_upgrade'))
|
return HttpResponseRedirect(reverse('corporate.views.initial_upgrade'))
|
||||||
|
|
||||||
|
if customer.sponsorship_pending:
|
||||||
|
if user.has_billing_access:
|
||||||
|
context = {"admin_access": True, "sponsorship_pending": True}
|
||||||
|
else:
|
||||||
|
context = {"admin_access": False}
|
||||||
|
return render(request, 'corporate/billing.html', context=context)
|
||||||
|
|
||||||
if not CustomerPlan.objects.filter(customer=customer).exists():
|
if not CustomerPlan.objects.filter(customer=customer).exists():
|
||||||
return HttpResponseRedirect(reverse('corporate.views.initial_upgrade'))
|
return HttpResponseRedirect(reverse('corporate.views.initial_upgrade'))
|
||||||
|
|
||||||
if not user.is_realm_admin and not user.is_billing_admin:
|
if not user.has_billing_access:
|
||||||
context: Dict[str, Any] = {'admin_access': False}
|
context = {'admin_access': False}
|
||||||
return render(request, 'corporate/billing.html', context=context)
|
return render(request, 'corporate/billing.html', context=context)
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
|
|
|
@ -139,6 +139,11 @@ run_test('create_ajax_request', () => {
|
||||||
state.location_reload += 1;
|
state.location_reload += 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window.location.replace = (reload_to) => {
|
||||||
|
state.location_reload += 1;
|
||||||
|
assert.equal(reload_to, "/billing");
|
||||||
|
};
|
||||||
|
|
||||||
success();
|
success();
|
||||||
|
|
||||||
assert.equal(state.location_reload, 1);
|
assert.equal(state.location_reload, 1);
|
||||||
|
|
|
@ -34,12 +34,22 @@ run_test("initialize", () => {
|
||||||
assert.equal(page_name, "upgrade");
|
assert.equal(page_name, "upgrade");
|
||||||
};
|
};
|
||||||
|
|
||||||
helpers.create_ajax_request = (url, form_name, stripe_token) => {
|
helpers.create_ajax_request = (url, form_name, stripe_token, numeric_inputs, redirect_to) => {
|
||||||
assert.equal(url, "/json/billing/upgrade");
|
|
||||||
if (form_name === "autopay") {
|
if (form_name === "autopay") {
|
||||||
|
assert.equal(url, "/json/billing/upgrade");
|
||||||
assert.equal(stripe_token, "stripe_add_card_token");
|
assert.equal(stripe_token, "stripe_add_card_token");
|
||||||
|
assert.deepEqual(numeric_inputs, ["licenses"]);
|
||||||
|
assert.equal(redirect_to, undefined);
|
||||||
} else if (form_name === "invoice") {
|
} else if (form_name === "invoice") {
|
||||||
|
assert.equal(url, "/json/billing/upgrade");
|
||||||
assert.equal(stripe_token, undefined);
|
assert.equal(stripe_token, undefined);
|
||||||
|
assert.deepEqual(numeric_inputs, ["licenses"]);
|
||||||
|
assert.equal(redirect_to, undefined);
|
||||||
|
} else if (form_name === "sponsorship") {
|
||||||
|
assert.equal(url, "/json/billing/sponsorship");
|
||||||
|
assert.equal(stripe_token, undefined);
|
||||||
|
assert.equal(numeric_inputs, undefined);
|
||||||
|
assert.equal(redirect_to, "/");
|
||||||
} else {
|
} else {
|
||||||
throw Error("Unhandled case");
|
throw Error("Unhandled case");
|
||||||
}
|
}
|
||||||
|
@ -98,6 +108,7 @@ run_test("initialize", () => {
|
||||||
|
|
||||||
const add_card_click_handler = $('#add-card-button').get_on_handler('click');
|
const add_card_click_handler = $('#add-card-button').get_on_handler('click');
|
||||||
const invoice_click_handler = $('#invoice-button').get_on_handler('click');
|
const invoice_click_handler = $('#invoice-button').get_on_handler('click');
|
||||||
|
const request_sponsorship_click_handler = $('#sponsorship-button').get_on_handler('click');
|
||||||
|
|
||||||
helpers.is_valid_input = () => {
|
helpers.is_valid_input = () => {
|
||||||
return true;
|
return true;
|
||||||
|
@ -112,6 +123,8 @@ run_test("initialize", () => {
|
||||||
add_card_click_handler(e);
|
add_card_click_handler(e);
|
||||||
invoice_click_handler(e);
|
invoice_click_handler(e);
|
||||||
|
|
||||||
|
request_sponsorship_click_handler(e);
|
||||||
|
|
||||||
helpers.show_license_section = (section) => {
|
helpers.show_license_section = (section) => {
|
||||||
assert.equal(section, "manual");
|
assert.equal(section, "manual");
|
||||||
};
|
};
|
||||||
|
@ -131,6 +144,26 @@ run_test("initialize", () => {
|
||||||
assert.equal($("#autopay_monthly_price").text(), "6.40");
|
assert.equal($("#autopay_monthly_price").text(), "6.40");
|
||||||
assert.equal($("#invoice_annual_price").text(), "64");
|
assert.equal($("#invoice_annual_price").text(), "64");
|
||||||
assert.equal($("#invoice_annual_price_per_month").text(), "5.34");
|
assert.equal($("#invoice_annual_price_per_month").text(), "5.34");
|
||||||
|
|
||||||
|
const organization_type_change_handler = $('select[name=organization-type]').get_on_handler('change');
|
||||||
|
organization_type_change_handler.call({value: "open_source"});
|
||||||
|
assert.equal($("#sponsorship-discount-details").text(),
|
||||||
|
"Open source projects are eligible for fully sponsored (free) Zulip Standard.");
|
||||||
|
organization_type_change_handler.call({value: "research"});
|
||||||
|
assert.equal($("#sponsorship-discount-details").text(),
|
||||||
|
"Academic research organizations are eligible for fully sponsored (free) Zulip Standard.");
|
||||||
|
organization_type_change_handler.call({value: "event"});
|
||||||
|
assert.equal($("#sponsorship-discount-details").text(),
|
||||||
|
"Events are eligible for fully sponsored (free) Zulip Standard.");
|
||||||
|
organization_type_change_handler.call({value: "education"});
|
||||||
|
assert.equal($("#sponsorship-discount-details").text(),
|
||||||
|
"Education use is eligible for an 85%-100% discount.");
|
||||||
|
organization_type_change_handler.call({value: "non_profit"});
|
||||||
|
assert.equal($("#sponsorship-discount-details").text(),
|
||||||
|
"Nonprofits are eligible for an 85%-100% discount.");
|
||||||
|
organization_type_change_handler.call({value: "other"});
|
||||||
|
assert.equal($("#sponsorship-discount-details").text(),
|
||||||
|
"Your organization might be eligible for a discount or sponsorship.");
|
||||||
});
|
});
|
||||||
|
|
||||||
run_test("autopay_form_fields", () => {
|
run_test("autopay_form_fields", () => {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
exports.create_ajax_request = function (url, form_name, stripe_token = null, numeric_inputs = []) {
|
exports.create_ajax_request = function (url, form_name, stripe_token = null, numeric_inputs = [], redirect_to = "/billing") {
|
||||||
const form = $("#" + form_name + "-form");
|
const form = $("#" + form_name + "-form");
|
||||||
const form_loading_indicator = "#" + form_name + "_loading_indicator";
|
const form_loading_indicator = "#" + form_name + "_loading_indicator";
|
||||||
const form_input_section = "#" + form_name + "-input-section";
|
const form_input_section = "#" + form_name + "-input-section";
|
||||||
|
@ -44,7 +44,7 @@ exports.create_ajax_request = function (url, form_name, stripe_token = null, num
|
||||||
location.hash = "";
|
location.hash = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
location.reload();
|
window.location.replace(redirect_to);
|
||||||
},
|
},
|
||||||
error: function (xhr) {
|
error: function (xhr) {
|
||||||
$(form_loading).hide();
|
$(form_loading).hide();
|
||||||
|
@ -75,6 +75,18 @@ exports.update_charged_amount = function (prices, schedule) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.update_discount_details = function (organization_type) {
|
||||||
|
const discount_details = {
|
||||||
|
open_source: "Open source projects are eligible for fully sponsored (free) Zulip Standard.",
|
||||||
|
research: "Academic research organizations are eligible for fully sponsored (free) Zulip Standard.",
|
||||||
|
non_profit: "Nonprofits are eligible for an 85%-100% discount.",
|
||||||
|
event: "Events are eligible for fully sponsored (free) Zulip Standard.",
|
||||||
|
education: "Education use is eligible for an 85%-100% discount.",
|
||||||
|
other: "Your organization might be eligible for a discount or sponsorship.",
|
||||||
|
};
|
||||||
|
$("#sponsorship-discount-details").text(discount_details[organization_type]);
|
||||||
|
};
|
||||||
|
|
||||||
exports.show_license_section = function (license) {
|
exports.show_license_section = function (license) {
|
||||||
$("#license-automatic-section").hide();
|
$("#license-automatic-section").hide();
|
||||||
$("#license-manual-section").hide();
|
$("#license-manual-section").hide();
|
||||||
|
|
|
@ -36,6 +36,11 @@ exports.initialize = () => {
|
||||||
helpers.create_ajax_request("/json/billing/upgrade", "invoice", undefined, ["licenses"]);
|
helpers.create_ajax_request("/json/billing/upgrade", "invoice", undefined, ["licenses"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("#sponsorship-button").on("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
helpers.create_ajax_request("/json/billing/sponsorship", "sponsorship", undefined, undefined, "/");
|
||||||
|
});
|
||||||
|
|
||||||
const prices = {};
|
const prices = {};
|
||||||
prices.annual = page_params.annual_price * (1 - page_params.percent_off / 100);
|
prices.annual = page_params.annual_price * (1 - page_params.percent_off / 100);
|
||||||
prices.monthly = page_params.monthly_price * (1 - page_params.percent_off / 100);
|
prices.monthly = page_params.monthly_price * (1 - page_params.percent_off / 100);
|
||||||
|
@ -48,6 +53,10 @@ exports.initialize = () => {
|
||||||
helpers.update_charged_amount(prices, this.value);
|
helpers.update_charged_amount(prices, this.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$('select[name=organization-type]').on("change", function () {
|
||||||
|
helpers.update_discount_details(this.value);
|
||||||
|
});
|
||||||
|
|
||||||
$("#autopay_annual_price").text(helpers.format_money(prices.annual));
|
$("#autopay_annual_price").text(helpers.format_money(prices.annual));
|
||||||
$("#autopay_annual_price_per_month").text(helpers.format_money(prices.annual / 12));
|
$("#autopay_annual_price_per_month").text(helpers.format_money(prices.annual / 12));
|
||||||
$("#autopay_monthly_price").text(helpers.format_money(prices.monthly));
|
$("#autopay_monthly_price").text(helpers.format_money(prices.monthly));
|
||||||
|
|
|
@ -79,6 +79,11 @@ tr.admin td:first-child {
|
||||||
top: -25px;
|
top: -25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sponsorship-pending-form {
|
||||||
|
position: relative;
|
||||||
|
top: -25px;
|
||||||
|
}
|
||||||
|
|
||||||
.scrub-realm-form {
|
.scrub-realm-form {
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -50px;
|
top: -50px;
|
||||||
|
|
|
@ -177,6 +177,7 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sponsorship-loading,
|
||||||
#planchange-loading,
|
#planchange-loading,
|
||||||
#cardchange-loading,
|
#cardchange-loading,
|
||||||
#invoice-loading,
|
#invoice-loading,
|
||||||
|
@ -186,7 +187,7 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sponsorship-success,
|
||||||
#planchange-success,
|
#planchange-success,
|
||||||
#cardchange-success,
|
#cardchange-success,
|
||||||
#invoice-success,
|
#invoice-success,
|
||||||
|
@ -195,6 +196,7 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sponsorship-error,
|
||||||
#planchange-error,
|
#planchange-error,
|
||||||
#cardchange-error,
|
#cardchange-error,
|
||||||
#invoice-error,
|
#invoice-error,
|
||||||
|
@ -224,6 +226,7 @@
|
||||||
stroke: hsl(0, 0%, 100%);
|
stroke: hsl(0, 0%, 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sponsorship_loading_indicator,
|
||||||
#planchange_loading_indicator,
|
#planchange_loading_indicator,
|
||||||
#cardchange_loading_indicator,
|
#cardchange_loading_indicator,
|
||||||
#invoice_loading_indicator,
|
#invoice_loading_indicator,
|
||||||
|
@ -231,6 +234,7 @@
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sponsorship_loading_indicator_box_container,
|
||||||
#planchange_loading_indicator_box_container,
|
#planchange_loading_indicator_box_container,
|
||||||
#cardchange_loading_indicator_box_container,
|
#cardchange_loading_indicator_box_container,
|
||||||
#invoice_loading_indicator_box_container,
|
#invoice_loading_indicator_box_container,
|
||||||
|
@ -239,6 +243,7 @@
|
||||||
left: 50%;
|
left: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sponsorship_loading_indicator_box,
|
||||||
#planchange_loading_indicator_box,
|
#planchange_loading_indicator_box,
|
||||||
#cardchange_loading_indicator_box,
|
#cardchange_loading_indicator_box,
|
||||||
#invoice_loading_indicator_box,
|
#invoice_loading_indicator_box,
|
||||||
|
@ -250,6 +255,7 @@
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sponsorship_loading_indicator .loading_indicator_text,
|
||||||
#planchange_loading_indicator .loading_indicator_text,
|
#planchange_loading_indicator .loading_indicator_text,
|
||||||
#cardchange_loading_indicator .loading_indicator_text,
|
#cardchange_loading_indicator .loading_indicator_text,
|
||||||
#invoice_loading_indicator .loading_indicator_text,
|
#invoice_loading_indicator .loading_indicator_text,
|
||||||
|
|
|
@ -30,6 +30,16 @@
|
||||||
</select>
|
</select>
|
||||||
<button type="submit" class="button rounded small support-submit-button">Update</button>
|
<button type="submit" class="button rounded small support-submit-button">Update</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="POST" class="sponsorship-pending-form">
|
||||||
|
<b>Sponsorship pending</b>:<br>
|
||||||
|
{{ csrf_input }}
|
||||||
|
<input type="hidden" name="realm_id" value="{{ realm.id }}" />
|
||||||
|
<select name="sponsorship_pending">
|
||||||
|
<option value="true" {% if realm.customer and realm.customer.sponsorship_pending %}selected{% endif %}>Yes</option>
|
||||||
|
<option value="false" {% if not realm.customer or not realm.customer.sponsorship_pending %}selected{% endif %}>No</option>
|
||||||
|
</select>
|
||||||
|
<button type="submit" class="button rounded small support-submit-button">Update</button>
|
||||||
|
</form>
|
||||||
<form method="POST" class="support-discount-form">
|
<form method="POST" class="support-discount-form">
|
||||||
<b>Discount (use 85 for nonprofits)</b>:<br>
|
<b>Discount (use 85 for nonprofits)</b>:<br>
|
||||||
{{ csrf_input }}
|
{{ csrf_input }}
|
||||||
|
|
|
@ -152,6 +152,9 @@
|
||||||
</div>
|
</div>
|
||||||
{% elif admin_access and not has_active_plan %}
|
{% elif admin_access and not has_active_plan %}
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
|
{% if sponsorship_pending %}
|
||||||
|
<h3>Your organization has requested sponsored or discounted hosting.</h3>
|
||||||
|
{% else %}
|
||||||
<center>
|
<center>
|
||||||
<p>
|
<p>
|
||||||
<h2>Your organization is on the <b>Zulip Free</b> plan.</h2>
|
<h2>Your organization is on the <b>Zulip Free</b> plan.</h2>
|
||||||
|
@ -163,6 +166,7 @@
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</center>
|
</center>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="support-link">
|
<div class="support-link">
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
<ul class="nav nav-tabs" id="upgrade-tabs">
|
<ul class="nav nav-tabs" id="upgrade-tabs">
|
||||||
<li class="active"><a data-toggle="tab" href="#autopay">💳 Pay automatically</a></li>
|
<li class="active"><a data-toggle="tab" href="#autopay">💳 Pay automatically</a></li>
|
||||||
<li><a data-toggle="tab" href="#invoice">🧾 Pay by invoice</a></li>
|
<li><a data-toggle="tab" href="#invoice">🧾 Pay by invoice</a></li>
|
||||||
|
<li><a data-toggle="tab" href="#sponsorship">💚 Request sponsorship</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||||
|
@ -218,6 +219,53 @@
|
||||||
Upgrade complete! The page will now reload.
|
Upgrade complete! The page will now reload.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-pane" id="sponsorship">
|
||||||
|
<div id="sponsorship-error" class="alert alert-danger"></div>
|
||||||
|
<div id="sponsorship-input-section">
|
||||||
|
<form id="sponsorship-form" method="post">
|
||||||
|
<label>
|
||||||
|
<h4>Organization type</h4>
|
||||||
|
</label>
|
||||||
|
<select name="organization-type" required style="width: 100%;">
|
||||||
|
<option disabled selected> -- select --</option>
|
||||||
|
<option value="open_source">{{_('Open source')}}</option>
|
||||||
|
<option value="research">{{_('Academic research')}}</option>
|
||||||
|
<option value="education">{{_('Education')}}</option>
|
||||||
|
<option value="non_profit">{{_('Non-Profit')}}</option>
|
||||||
|
<option value="event">{{_('Event (hackathons, conferences, etc.)')}}</option>
|
||||||
|
<option value="other">{{_('Other')}}</option>
|
||||||
|
</select>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
<h4>Organization website</h4>
|
||||||
|
</label>
|
||||||
|
<input name="website" style="width: 100%;" type="text" class="input-large" required/>
|
||||||
|
<label>
|
||||||
|
<h4>Describe your organization briefly</h4>
|
||||||
|
</label>
|
||||||
|
<textarea name="description" style="width: 100%;" cols="100" rows="5" required></textarea>
|
||||||
|
<br>
|
||||||
|
<p id="sponsorship-discount-details"></p>
|
||||||
|
<button type="submit" id="sponsorship-button" class="stripe-button-el invoice-button">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="sponsorship-loading">
|
||||||
|
<div class="zulip-loading-logo">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 40 40" version="1.1">
|
||||||
|
<g transform="translate(-297.14285,-466.64792)">
|
||||||
|
<circle cx="317.14285" cy="486.64792" r="19.030317" style="stroke-width:1.93936479;"/>
|
||||||
|
<path d="m309.24286 477.14791 14.2 0 1.6 3.9-11.2 11.9 9.6 0 1.6 3.2-14.2 0-1.6-3.9 11.2-11.9-9.6 0z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div id="sponsorship_loading_indicator"></div>
|
||||||
|
</div>
|
||||||
|
<div id="sponsorship-success" class="alert alert-success">
|
||||||
|
Request received! The page will now reload.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="support-link">
|
<div class="support-link">
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
{% extends "zerver/emails/email_base_messages.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<b>Support URL</b>: <a href="{{ support_url }}">{{ support_url }}</a>
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<b>Website</b>: <a href="{{ website }}">{{ website }}</a>
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<b>Organization type</b>: {{ organization_type }}
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<b>Description</b>:
|
||||||
|
<br>
|
||||||
|
{{ description }}
|
||||||
|
|
||||||
|
<br><br>
|
||||||
|
|
||||||
|
<b>Requested by</b>: {{ requested_by }} · <b>User role</b>: {{ user_role }} · <b>String ID</b>: {{ string_id }}
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1 @@
|
||||||
|
{% trans %}Sponsorship request ({{ organization_type }}) for {{ string_id }}{% endtrans %}
|
|
@ -0,0 +1,10 @@
|
||||||
|
Support URL: {{ support_url }}
|
||||||
|
|
||||||
|
Website: {{ website }}
|
||||||
|
|
||||||
|
Organization type: {{ organization_type }}
|
||||||
|
|
||||||
|
Description:
|
||||||
|
{{ description }}
|
||||||
|
|
||||||
|
Requested by: {{ requested_by }} · User role: {{ user_role }} · String ID: {{ string_id }}
|
|
@ -45,7 +45,7 @@
|
||||||
<a href="/new/" class="button green">
|
<a href="/new/" class="button green">
|
||||||
Sign up now
|
Sign up now
|
||||||
</a>
|
</a>
|
||||||
{% elif realm_plan_type == 2 %}
|
{% elif realm_plan_type == 2 or sponsorship_pending %}
|
||||||
<div class="pricing-details"></div>
|
<div class="pricing-details"></div>
|
||||||
<a href='/upgrade' class="button black-current-value" type="button">
|
<a href='/upgrade' class="button black-current-value" type="button">
|
||||||
Current plan
|
Current plan
|
||||||
|
@ -93,6 +93,10 @@
|
||||||
Buy Standard
|
Buy Standard
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
|
{% elif sponsorship_pending %}
|
||||||
|
<a href="/billing" class="button black-current-value" type="button">
|
||||||
|
Sponsorship pending
|
||||||
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/upgrade" class="button green">
|
<a href="/upgrade" class="button green">
|
||||||
{% if free_trial_days %}
|
{% if free_trial_days %}
|
||||||
|
|
|
@ -1188,6 +1188,10 @@ class UserProfile(AbstractBaseUser, PermissionsMixin):
|
||||||
# ROLE_GUEST to ROLE_MEMBER here.
|
# ROLE_GUEST to ROLE_MEMBER here.
|
||||||
self.role = UserProfile.ROLE_MEMBER
|
self.role = UserProfile.ROLE_MEMBER
|
||||||
|
|
||||||
|
@property
|
||||||
|
def has_billing_access(self) -> bool:
|
||||||
|
return self.is_realm_admin or self.is_billing_admin
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_realm_owner(self) -> bool:
|
def is_realm_owner(self) -> bool:
|
||||||
return self.role == UserProfile.ROLE_REALM_OWNER
|
return self.role == UserProfile.ROLE_REALM_OWNER
|
||||||
|
|
|
@ -8,6 +8,7 @@ from django.conf import settings
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
|
|
||||||
|
from corporate.models import Customer
|
||||||
from zerver.lib.integrations import INTEGRATIONS
|
from zerver.lib.integrations import INTEGRATIONS
|
||||||
from zerver.lib.test_classes import ZulipTestCase
|
from zerver.lib.test_classes import ZulipTestCase
|
||||||
from zerver.lib.test_helpers import HostRequestMock
|
from zerver.lib.test_helpers import HostRequestMock
|
||||||
|
@ -383,11 +384,12 @@ class PlansPageTest(ZulipTestCase):
|
||||||
sign_up_now = "Sign up now"
|
sign_up_now = "Sign up now"
|
||||||
buy_standard = "Buy Standard"
|
buy_standard = "Buy Standard"
|
||||||
current_plan = "Current plan"
|
current_plan = "Current plan"
|
||||||
|
sponsorship_pending = "Sponsorship pending"
|
||||||
|
|
||||||
# Root domain
|
# Root domain
|
||||||
result = self.client_get("/plans/", subdomain="")
|
result = self.client_get("/plans/", subdomain="")
|
||||||
self.assert_in_success_response([sign_up_now, buy_standard], result)
|
self.assert_in_success_response([sign_up_now, buy_standard], result)
|
||||||
self.assert_not_in_success_response([current_plan], result)
|
self.assert_not_in_success_response([current_plan, sponsorship_pending], result)
|
||||||
|
|
||||||
realm = get_realm("zulip")
|
realm = get_realm("zulip")
|
||||||
realm.plan_type = Realm.SELF_HOSTED
|
realm.plan_type = Realm.SELF_HOSTED
|
||||||
|
@ -408,24 +410,32 @@ class PlansPageTest(ZulipTestCase):
|
||||||
# But in the development environment, it renders a page
|
# But in the development environment, it renders a page
|
||||||
result = self.client_get("/plans/", subdomain="zulip")
|
result = self.client_get("/plans/", subdomain="zulip")
|
||||||
self.assert_in_success_response([sign_up_now, buy_standard], result)
|
self.assert_in_success_response([sign_up_now, buy_standard], result)
|
||||||
self.assert_not_in_success_response([current_plan], result)
|
self.assert_not_in_success_response([current_plan, sponsorship_pending], result)
|
||||||
|
|
||||||
realm.plan_type = Realm.LIMITED
|
realm.plan_type = Realm.LIMITED
|
||||||
realm.save(update_fields=["plan_type"])
|
realm.save(update_fields=["plan_type"])
|
||||||
result = self.client_get("/plans/", subdomain="zulip")
|
result = self.client_get("/plans/", subdomain="zulip")
|
||||||
self.assert_in_success_response([current_plan, buy_standard], result)
|
self.assert_in_success_response([current_plan, buy_standard], result)
|
||||||
self.assert_not_in_success_response([sign_up_now], result)
|
self.assert_not_in_success_response([sign_up_now, sponsorship_pending], result)
|
||||||
|
|
||||||
realm.plan_type = Realm.STANDARD_FREE
|
realm.plan_type = Realm.STANDARD_FREE
|
||||||
realm.save(update_fields=["plan_type"])
|
realm.save(update_fields=["plan_type"])
|
||||||
result = self.client_get("/plans/", subdomain="zulip")
|
result = self.client_get("/plans/", subdomain="zulip")
|
||||||
self.assert_in_success_response([current_plan], result)
|
self.assert_in_success_response([current_plan], result)
|
||||||
self.assert_not_in_success_response([sign_up_now, buy_standard], result)
|
self.assert_not_in_success_response([sign_up_now, buy_standard, sponsorship_pending], result)
|
||||||
|
|
||||||
realm.plan_type = Realm.STANDARD
|
realm.plan_type = Realm.STANDARD
|
||||||
realm.save(update_fields=["plan_type"])
|
realm.save(update_fields=["plan_type"])
|
||||||
result = self.client_get("/plans/", subdomain="zulip")
|
result = self.client_get("/plans/", subdomain="zulip")
|
||||||
self.assert_in_success_response([current_plan], result)
|
self.assert_in_success_response([current_plan], result)
|
||||||
|
self.assert_not_in_success_response([sign_up_now, buy_standard, sponsorship_pending], result)
|
||||||
|
|
||||||
|
realm.plan_type = Realm.LIMITED
|
||||||
|
realm.save()
|
||||||
|
Customer.objects.create(realm=get_realm("zulip"), stripe_customer_id="cus_id", sponsorship_pending=True)
|
||||||
|
result = self.client_get("/plans/", subdomain="zulip")
|
||||||
|
self.assert_in_success_response([current_plan], result)
|
||||||
|
self.assert_in_success_response([current_plan, sponsorship_pending], result)
|
||||||
self.assert_not_in_success_response([sign_up_now, buy_standard], result)
|
self.assert_not_in_success_response([sign_up_now, buy_standard], result)
|
||||||
|
|
||||||
class AppsPageTest(ZulipTestCase):
|
class AppsPageTest(ZulipTestCase):
|
||||||
|
|
|
@ -697,17 +697,38 @@ class HomeTest(ZulipTestCase):
|
||||||
self.assertIn('Billing', result_html)
|
self.assertIn('Billing', result_html)
|
||||||
|
|
||||||
# billing admin, with CustomerPlan -> show billing link
|
# billing admin, with CustomerPlan -> show billing link
|
||||||
user.role = UserProfile.ROLE_REALM_ADMINISTRATOR
|
user.role = UserProfile.ROLE_MEMBER
|
||||||
user.is_billing_admin = True
|
user.is_billing_admin = True
|
||||||
user.save(update_fields=['role', 'is_billing_admin'])
|
user.save(update_fields=['role', 'is_billing_admin'])
|
||||||
result_html = self._get_home_page().content.decode('utf-8')
|
result_html = self._get_home_page().content.decode('utf-8')
|
||||||
self.assertIn('Billing', result_html)
|
self.assertIn('Billing', result_html)
|
||||||
|
|
||||||
|
# member, with CustomerPlan -> no billing link
|
||||||
|
user.is_billing_admin = False
|
||||||
|
user.save(update_fields=['is_billing_admin'])
|
||||||
|
result_html = self._get_home_page().content.decode('utf-8')
|
||||||
|
self.assertNotIn('Billing', result_html)
|
||||||
|
|
||||||
|
# guest, with CustomerPlan -> no billing link
|
||||||
|
user.role = UserProfile.ROLE_GUEST
|
||||||
|
user.save(update_fields=['role'])
|
||||||
|
result_html = self._get_home_page().content.decode('utf-8')
|
||||||
|
self.assertNotIn('Billing', result_html)
|
||||||
|
|
||||||
# billing admin, but no CustomerPlan -> no billing link
|
# billing admin, but no CustomerPlan -> no billing link
|
||||||
|
user.role = UserProfile.ROLE_MEMBER
|
||||||
|
user.is_billing_admin = True
|
||||||
|
user.save(update_fields=['role', 'is_billing_admin'])
|
||||||
CustomerPlan.objects.all().delete()
|
CustomerPlan.objects.all().delete()
|
||||||
result_html = self._get_home_page().content.decode('utf-8')
|
result_html = self._get_home_page().content.decode('utf-8')
|
||||||
self.assertNotIn('Billing', result_html)
|
self.assertNotIn('Billing', result_html)
|
||||||
|
|
||||||
|
# billing admin, with sponsorship pending -> show billing link
|
||||||
|
customer.sponsorship_pending = True
|
||||||
|
customer.save(update_fields=["sponsorship_pending"])
|
||||||
|
result_html = self._get_home_page().content.decode('utf-8')
|
||||||
|
self.assertIn('Billing', result_html)
|
||||||
|
|
||||||
# billing admin, no customer object -> make sure it doesn't crash
|
# billing admin, no customer object -> make sure it doesn't crash
|
||||||
customer.delete()
|
customer.delete()
|
||||||
result = self._get_home_page()
|
result = self._get_home_page()
|
||||||
|
|
|
@ -291,10 +291,14 @@ def home_real(request: HttpRequest) -> HttpResponse:
|
||||||
show_plans = False
|
show_plans = False
|
||||||
if settings.CORPORATE_ENABLED and user_profile is not None:
|
if settings.CORPORATE_ENABLED and user_profile is not None:
|
||||||
from corporate.models import Customer, CustomerPlan
|
from corporate.models import Customer, CustomerPlan
|
||||||
if user_profile.is_billing_admin or user_profile.is_realm_admin:
|
if user_profile.has_billing_access:
|
||||||
customer = Customer.objects.filter(realm=user_profile.realm).first()
|
customer = Customer.objects.filter(realm=user_profile.realm).first()
|
||||||
if customer is not None and CustomerPlan.objects.filter(customer=customer).exists():
|
if customer is not None:
|
||||||
show_billing = True
|
if customer.sponsorship_pending:
|
||||||
|
show_billing = True
|
||||||
|
elif CustomerPlan.objects.filter(customer=customer).exists():
|
||||||
|
show_billing = True
|
||||||
|
|
||||||
if user_profile.realm.plan_type == Realm.LIMITED:
|
if user_profile.realm.plan_type == Realm.LIMITED:
|
||||||
show_plans = True
|
show_plans = True
|
||||||
|
|
||||||
|
|
|
@ -28,16 +28,25 @@ def plans_view(request: HttpRequest) -> HttpResponse:
|
||||||
realm = get_realm_from_request(request)
|
realm = get_realm_from_request(request)
|
||||||
realm_plan_type = 0
|
realm_plan_type = 0
|
||||||
free_trial_days = settings.FREE_TRIAL_DAYS
|
free_trial_days = settings.FREE_TRIAL_DAYS
|
||||||
|
sponsorship_pending = False
|
||||||
|
|
||||||
if realm is not None:
|
if realm is not None:
|
||||||
realm_plan_type = realm.plan_type
|
realm_plan_type = realm.plan_type
|
||||||
if realm.plan_type == Realm.SELF_HOSTED and settings.PRODUCTION:
|
if realm.plan_type == Realm.SELF_HOSTED and settings.PRODUCTION:
|
||||||
return HttpResponseRedirect('https://zulip.com/plans')
|
return HttpResponseRedirect('https://zulip.com/plans')
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return redirect_to_login(next="plans")
|
return redirect_to_login(next="plans")
|
||||||
|
|
||||||
|
if settings.CORPORATE_ENABLED:
|
||||||
|
from corporate.models import get_customer_by_realm
|
||||||
|
customer = get_customer_by_realm(realm)
|
||||||
|
if customer is not None:
|
||||||
|
sponsorship_pending = customer.sponsorship_pending
|
||||||
|
|
||||||
return TemplateResponse(
|
return TemplateResponse(
|
||||||
request,
|
request,
|
||||||
"zerver/plans.html",
|
"zerver/plans.html",
|
||||||
context={"realm_plan_type": realm_plan_type, 'free_trial_days': free_trial_days},
|
context={"realm_plan_type": realm_plan_type, 'free_trial_days': free_trial_days, 'sponsorship_pending': sponsorship_pending},
|
||||||
)
|
)
|
||||||
|
|
||||||
@add_google_analytics
|
@add_google_analytics
|
||||||
|
|
|
@ -66,6 +66,7 @@ NOTIFICATION_BOT = "notification-bot@zulip.com"
|
||||||
ERROR_BOT = "error-bot@zulip.com"
|
ERROR_BOT = "error-bot@zulip.com"
|
||||||
EMAIL_GATEWAY_BOT = "emailgateway@zulip.com"
|
EMAIL_GATEWAY_BOT = "emailgateway@zulip.com"
|
||||||
PHYSICAL_ADDRESS = "Zulip Headquarters, 123 Octo Stream, South Pacific Ocean"
|
PHYSICAL_ADDRESS = "Zulip Headquarters, 123 Octo Stream, South Pacific Ocean"
|
||||||
|
STAFF_SUBDOMAIN = "zulip"
|
||||||
EXTRA_INSTALLED_APPS = ["zilencer", "analytics", "corporate"]
|
EXTRA_INSTALLED_APPS = ["zilencer", "analytics", "corporate"]
|
||||||
# Disable Camo in development
|
# Disable Camo in development
|
||||||
CAMO_URI = ''
|
CAMO_URI = ''
|
||||||
|
|
Loading…
Reference in New Issue