mirror of https://github.com/zulip/zulip.git
billing: Add BillingSession support for requesting sponsorship.
This commit is contained in:
parent
1691205306
commit
a01618d633
|
@ -11,6 +11,7 @@ from functools import wraps
|
||||||
from typing import Any, Callable, Dict, Generator, Optional, Tuple, TypedDict, TypeVar, Union
|
from typing import Any, Callable, Dict, Generator, Optional, Tuple, TypedDict, TypeVar, Union
|
||||||
|
|
||||||
import stripe
|
import stripe
|
||||||
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
from django.core.signing import Signer
|
from django.core.signing import Signer
|
||||||
|
@ -28,6 +29,7 @@ from corporate.models import (
|
||||||
LicenseLedger,
|
LicenseLedger,
|
||||||
PaymentIntent,
|
PaymentIntent,
|
||||||
Session,
|
Session,
|
||||||
|
ZulipSponsorshipRequest,
|
||||||
get_current_plan_by_customer,
|
get_current_plan_by_customer,
|
||||||
get_current_plan_by_realm,
|
get_current_plan_by_realm,
|
||||||
get_customer_by_realm,
|
get_customer_by_realm,
|
||||||
|
@ -36,10 +38,20 @@ from corporate.models import (
|
||||||
)
|
)
|
||||||
from zerver.lib.exceptions import JsonableError
|
from zerver.lib.exceptions import JsonableError
|
||||||
from zerver.lib.logging_util import log_to_file
|
from zerver.lib.logging_util import log_to_file
|
||||||
from zerver.lib.send_email import FromAddress, send_email_to_billing_admins_and_realm_owners
|
from zerver.lib.send_email import (
|
||||||
|
FromAddress,
|
||||||
|
send_email,
|
||||||
|
send_email_to_billing_admins_and_realm_owners,
|
||||||
|
)
|
||||||
from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime
|
from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime
|
||||||
from zerver.lib.utils import assert_is_not_none
|
from zerver.lib.utils import assert_is_not_none
|
||||||
from zerver.models import Realm, RealmAuditLog, UserProfile, get_system_bot
|
from zerver.models import (
|
||||||
|
Realm,
|
||||||
|
RealmAuditLog,
|
||||||
|
UserProfile,
|
||||||
|
get_org_type_display_name,
|
||||||
|
get_system_bot,
|
||||||
|
)
|
||||||
from zilencer.models import (
|
from zilencer.models import (
|
||||||
RemoteRealm,
|
RemoteRealm,
|
||||||
RemoteRealmAuditLog,
|
RemoteRealmAuditLog,
|
||||||
|
@ -533,6 +545,20 @@ class UpgradePageSessionTypeSpecificContext(TypedDict):
|
||||||
is_self_hosting: bool
|
is_self_hosting: bool
|
||||||
|
|
||||||
|
|
||||||
|
class SponsorshipApplicantInfo(TypedDict):
|
||||||
|
name: str
|
||||||
|
role: str
|
||||||
|
email: str
|
||||||
|
|
||||||
|
|
||||||
|
class SponsorshipRequestSessionSpecificContext(TypedDict):
|
||||||
|
# We don't store UserProfile for remote realms.
|
||||||
|
realm_user: Optional[UserProfile]
|
||||||
|
user_info: SponsorshipApplicantInfo
|
||||||
|
# TODO: Call this what we end up calling it for /support page.
|
||||||
|
realm_string_id: str
|
||||||
|
|
||||||
|
|
||||||
class UpgradePageContext(TypedDict):
|
class UpgradePageContext(TypedDict):
|
||||||
customer_name: str
|
customer_name: str
|
||||||
default_invoice_days_until_due: int
|
default_invoice_days_until_due: int
|
||||||
|
@ -552,12 +578,25 @@ class UpgradePageContext(TypedDict):
|
||||||
signed_seat_count: str
|
signed_seat_count: str
|
||||||
|
|
||||||
|
|
||||||
|
class SponsorshipRequestForm(forms.Form):
|
||||||
|
website = forms.URLField(max_length=ZulipSponsorshipRequest.MAX_ORG_URL_LENGTH, required=False)
|
||||||
|
organization_type = forms.IntegerField()
|
||||||
|
description = forms.CharField(widget=forms.Textarea)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
class BillingSession(ABC):
|
class BillingSession(ABC):
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def billing_session_url(self) -> str:
|
def billing_session_url(self) -> str:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def support_url(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_customer(self) -> Optional[Customer]:
|
def get_customer(self) -> Optional[Customer]:
|
||||||
pass
|
pass
|
||||||
|
@ -622,6 +661,16 @@ class BillingSession(ABC):
|
||||||
def is_sponsored_or_pending(self, customer: Optional[Customer]) -> bool:
|
def is_sponsored_or_pending(self, customer: Optional[Customer]) -> bool:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_sponsorship_request_session_specific_context(
|
||||||
|
self,
|
||||||
|
) -> SponsorshipRequestSessionSpecificContext:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def save_org_type_from_request_sponsorship_session(self, org_type: int) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_upgrade_page_session_type_specific_context(
|
def get_upgrade_page_session_type_specific_context(
|
||||||
self,
|
self,
|
||||||
|
@ -1717,6 +1766,67 @@ class BillingSession(ABC):
|
||||||
self.add_sponsorship_info_to_context(context)
|
self.add_sponsorship_info_to_context(context)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
def request_sponsorship(self, form: SponsorshipRequestForm) -> None:
|
||||||
|
if not form.is_valid():
|
||||||
|
message = " ".join(
|
||||||
|
error["message"]
|
||||||
|
for error_list in form.errors.get_json_data().values()
|
||||||
|
for error in error_list
|
||||||
|
)
|
||||||
|
raise BillingError("Form validation error", message=message)
|
||||||
|
|
||||||
|
request_context = self.get_sponsorship_request_session_specific_context()
|
||||||
|
with transaction.atomic():
|
||||||
|
# Ensures customer is created first before updating sponsorship status.
|
||||||
|
self.update_customer_sponsorship_status(True)
|
||||||
|
sponsorship_request = ZulipSponsorshipRequest(
|
||||||
|
customer=self.get_customer(),
|
||||||
|
requested_by=request_context["realm_user"],
|
||||||
|
org_website=form.cleaned_data["website"],
|
||||||
|
org_description=form.cleaned_data["description"],
|
||||||
|
org_type=form.cleaned_data["organization_type"],
|
||||||
|
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"],
|
||||||
|
)
|
||||||
|
sponsorship_request.save()
|
||||||
|
|
||||||
|
org_type = form.cleaned_data["organization_type"]
|
||||||
|
self.save_org_type_from_request_sponsorship_session(org_type)
|
||||||
|
|
||||||
|
if request_context["realm_user"] is not None:
|
||||||
|
# TODO: Refactor to not create an import cycle.
|
||||||
|
from zerver.actions.users import do_change_is_billing_admin
|
||||||
|
|
||||||
|
do_change_is_billing_admin(request_context["realm_user"], True)
|
||||||
|
|
||||||
|
org_type_display_name = get_org_type_display_name(org_type)
|
||||||
|
|
||||||
|
user_info = request_context["user_info"]
|
||||||
|
support_url = self.support_url()
|
||||||
|
context = {
|
||||||
|
"requested_by": user_info["name"],
|
||||||
|
"user_role": user_info["role"],
|
||||||
|
# TODO: realm_string_id needs to be replaced by something more generic.
|
||||||
|
"string_id": request_context["realm_string_id"],
|
||||||
|
"support_url": support_url,
|
||||||
|
"organization_type": org_type_display_name,
|
||||||
|
"website": sponsorship_request.org_website,
|
||||||
|
"description": sponsorship_request.org_description,
|
||||||
|
"expected_total_users": sponsorship_request.expected_total_users,
|
||||||
|
"paid_users_count": sponsorship_request.paid_users_count,
|
||||||
|
"paid_users_description": sponsorship_request.paid_users_description,
|
||||||
|
}
|
||||||
|
send_email(
|
||||||
|
"zerver/emails/sponsorship_request",
|
||||||
|
to_emails=[FromAddress.SUPPORT],
|
||||||
|
# Sent to the server's support team, so this email is not user-facing.
|
||||||
|
from_name="Zulip sponsorship request",
|
||||||
|
from_address=FromAddress.tokenized_no_reply_address(),
|
||||||
|
reply_to_email=user_info["email"],
|
||||||
|
context=context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RealmBillingSession(BillingSession):
|
class RealmBillingSession(BillingSession):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -1751,6 +1861,13 @@ class RealmBillingSession(BillingSession):
|
||||||
def billing_session_url(self) -> str:
|
def billing_session_url(self) -> str:
|
||||||
return self.realm.uri
|
return self.realm.uri
|
||||||
|
|
||||||
|
@override
|
||||||
|
def support_url(self) -> str:
|
||||||
|
# TODO: Refactor to not create an import cycle.
|
||||||
|
from corporate.lib.support import get_support_url
|
||||||
|
|
||||||
|
return get_support_url(self.realm)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def get_customer(self) -> Optional[Customer]:
|
def get_customer(self) -> Optional[Customer]:
|
||||||
return get_customer_by_realm(self.realm)
|
return get_customer_by_realm(self.realm)
|
||||||
|
@ -1987,10 +2104,34 @@ class RealmBillingSession(BillingSession):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def get_sponsorship_request_session_specific_context(
|
||||||
|
self,
|
||||||
|
) -> SponsorshipRequestSessionSpecificContext:
|
||||||
|
assert self.user is not None
|
||||||
|
return SponsorshipRequestSessionSpecificContext(
|
||||||
|
realm_user=self.user,
|
||||||
|
user_info=SponsorshipApplicantInfo(
|
||||||
|
name=self.user.full_name,
|
||||||
|
email=self.user.delivery_email,
|
||||||
|
role=self.user.get_role_name(),
|
||||||
|
),
|
||||||
|
realm_string_id=self.realm.string_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def save_org_type_from_request_sponsorship_session(self, org_type: int) -> None:
|
||||||
|
# TODO: Use the actions.py method for this.
|
||||||
|
if self.realm.org_type != org_type:
|
||||||
|
self.realm.org_type = org_type
|
||||||
|
self.realm.save(update_fields=["org_type"])
|
||||||
|
|
||||||
|
|
||||||
class RemoteRealmBillingSession(BillingSession): # nocoverage
|
class RemoteRealmBillingSession(BillingSession): # nocoverage
|
||||||
def __init__(
|
def __init__(
|
||||||
self, remote_realm: RemoteRealm, support_staff: Optional[UserProfile] = None
|
self,
|
||||||
|
remote_realm: RemoteRealm,
|
||||||
|
support_staff: Optional[UserProfile] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.remote_realm = remote_realm
|
self.remote_realm = remote_realm
|
||||||
if support_staff is not None:
|
if support_staff is not None:
|
||||||
|
@ -2004,6 +2145,10 @@ class RemoteRealmBillingSession(BillingSession): # nocoverage
|
||||||
def billing_session_url(self) -> str:
|
def billing_session_url(self) -> str:
|
||||||
return f"{settings.EXTERNAL_URI_SCHEME}{settings.SELF_HOSTING_MANAGEMENT_SUBDOMAIN}.{settings.EXTERNAL_HOST}/realm/{self.remote_realm.uuid}"
|
return f"{settings.EXTERNAL_URI_SCHEME}{settings.SELF_HOSTING_MANAGEMENT_SUBDOMAIN}.{settings.EXTERNAL_HOST}/realm/{self.remote_realm.uuid}"
|
||||||
|
|
||||||
|
@override
|
||||||
|
def support_url(self) -> str:
|
||||||
|
return "TODO:not-implemented"
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def get_customer(self) -> Optional[Customer]:
|
def get_customer(self) -> Optional[Customer]:
|
||||||
return get_customer_by_remote_realm(self.remote_realm)
|
return get_customer_by_remote_realm(self.remote_realm)
|
||||||
|
@ -2195,8 +2340,40 @@ class RemoteRealmBillingSession(BillingSession): # nocoverage
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def add_sponsorship_info_to_context(self, context: Dict[str, Any]) -> None:
|
def add_sponsorship_info_to_context(self, context: Dict[str, Any]) -> None:
|
||||||
# TBD
|
context.update(
|
||||||
pass
|
realm_org_type=self.remote_realm.org_type,
|
||||||
|
sorted_org_types=sorted(
|
||||||
|
(
|
||||||
|
[org_type_name, org_type]
|
||||||
|
for (org_type_name, org_type) in Realm.ORG_TYPES.items()
|
||||||
|
if not org_type.get("hidden")
|
||||||
|
),
|
||||||
|
key=self.sponsorship_org_type_key_helper,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def get_sponsorship_request_session_specific_context(
|
||||||
|
self,
|
||||||
|
) -> SponsorshipRequestSessionSpecificContext:
|
||||||
|
return SponsorshipRequestSessionSpecificContext(
|
||||||
|
realm_user=None,
|
||||||
|
user_info=SponsorshipApplicantInfo(
|
||||||
|
# TODO: Plumb through the session data on the acting user.
|
||||||
|
name="Remote realm administrator",
|
||||||
|
email=self.remote_realm.server.contact_email,
|
||||||
|
# TODO: Set user_role when determining which set of users can access the page.
|
||||||
|
role="Remote realm administrator",
|
||||||
|
),
|
||||||
|
# TODO: Check if this works on support page.
|
||||||
|
realm_string_id=self.remote_realm.host,
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def save_org_type_from_request_sponsorship_session(self, org_type: int) -> None:
|
||||||
|
if self.remote_realm.org_type != org_type:
|
||||||
|
self.remote_realm.org_type = org_type
|
||||||
|
self.remote_realm.save(update_fields=["org_type"])
|
||||||
|
|
||||||
|
|
||||||
class RemoteServerBillingSession(BillingSession): # nocoverage
|
class RemoteServerBillingSession(BillingSession): # nocoverage
|
||||||
|
@ -2204,7 +2381,9 @@ class RemoteServerBillingSession(BillingSession): # nocoverage
|
||||||
creating RemoteRealm objects."""
|
creating RemoteRealm objects."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, remote_server: RemoteZulipServer, support_staff: Optional[UserProfile] = None
|
self,
|
||||||
|
remote_server: RemoteZulipServer,
|
||||||
|
support_staff: Optional[UserProfile] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.remote_server = remote_server
|
self.remote_server = remote_server
|
||||||
if support_staff is not None:
|
if support_staff is not None:
|
||||||
|
@ -2218,6 +2397,10 @@ class RemoteServerBillingSession(BillingSession): # nocoverage
|
||||||
def billing_session_url(self) -> str:
|
def billing_session_url(self) -> str:
|
||||||
return "TBD"
|
return "TBD"
|
||||||
|
|
||||||
|
@override
|
||||||
|
def support_url(self) -> str:
|
||||||
|
return "TODO:not-implemented"
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def get_customer(self) -> Optional[Customer]:
|
def get_customer(self) -> Optional[Customer]:
|
||||||
return get_customer_by_remote_server(self.remote_server)
|
return get_customer_by_remote_server(self.remote_server)
|
||||||
|
@ -2401,8 +2584,42 @@ class RemoteServerBillingSession(BillingSession): # nocoverage
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def add_sponsorship_info_to_context(self, context: Dict[str, Any]) -> None:
|
def add_sponsorship_info_to_context(self, context: Dict[str, Any]) -> None:
|
||||||
# TBD
|
context.update(
|
||||||
pass
|
realm_org_type=self.remote_server.org_type,
|
||||||
|
sorted_org_types=sorted(
|
||||||
|
(
|
||||||
|
[org_type_name, org_type]
|
||||||
|
for (org_type_name, org_type) in Realm.ORG_TYPES.items()
|
||||||
|
if not org_type.get("hidden")
|
||||||
|
),
|
||||||
|
key=self.sponsorship_org_type_key_helper,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def get_sponsorship_request_session_specific_context(
|
||||||
|
self,
|
||||||
|
) -> SponsorshipRequestSessionSpecificContext:
|
||||||
|
return SponsorshipRequestSessionSpecificContext(
|
||||||
|
realm_user=None,
|
||||||
|
user_info=SponsorshipApplicantInfo(
|
||||||
|
# TODO: Figure out a better story here. We don't
|
||||||
|
# actually have a name or other details on the person
|
||||||
|
# doing this flow, but could ask for it in the login
|
||||||
|
# form if desired.
|
||||||
|
name="Remote server administrator",
|
||||||
|
email=self.remote_server.contact_email,
|
||||||
|
role="Remote server administrator",
|
||||||
|
),
|
||||||
|
# TODO: Check if this works on support page.
|
||||||
|
realm_string_id=self.remote_server.hostname,
|
||||||
|
)
|
||||||
|
|
||||||
|
@override
|
||||||
|
def save_org_type_from_request_sponsorship_session(self, org_type: int) -> None:
|
||||||
|
if self.remote_server.org_type != org_type:
|
||||||
|
self.remote_server.org_type = org_type
|
||||||
|
self.remote_server.save(update_fields=["org_type"])
|
||||||
|
|
||||||
|
|
||||||
def stripe_customer_has_credit_card_as_default_payment_method(
|
def stripe_customer_has_credit_card_as_default_payment_method(
|
||||||
|
|
|
@ -4134,7 +4134,7 @@ class RequiresBillingAccessTest(StripeTestCase):
|
||||||
pat
|
pat
|
||||||
for name in reverse_dict
|
for name in reverse_dict
|
||||||
for matches, pat, defaults, converters in reverse_dict.getlist(name)
|
for matches, pat, defaults, converters in reverse_dict.getlist(name)
|
||||||
if pat.startswith(re.escape("json/"))
|
if pat.startswith("json/") and not (pat.startswith(("json/realm/", "json/server/")))
|
||||||
}
|
}
|
||||||
self.assert_length(json_endpoints, len(tested_endpoints))
|
self.assert_length(json_endpoints, len(tested_endpoints))
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from django.conf.urls import include
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.views.generic import RedirectView, TemplateView
|
from django.views.generic import RedirectView, TemplateView
|
||||||
|
|
||||||
from corporate.views.billing_page import billing_home, sponsorship_request, update_plan
|
from corporate.views.billing_page import billing_home, update_plan
|
||||||
from corporate.views.event_status import event_status, event_status_page
|
from corporate.views.event_status import event_status, event_status_page
|
||||||
from corporate.views.portico import (
|
from corporate.views.portico import (
|
||||||
app_download_link_redirect,
|
app_download_link_redirect,
|
||||||
|
@ -27,8 +27,16 @@ from corporate.views.session import (
|
||||||
start_card_update_stripe_session,
|
start_card_update_stripe_session,
|
||||||
start_card_update_stripe_session_for_realm_upgrade,
|
start_card_update_stripe_session_for_realm_upgrade,
|
||||||
)
|
)
|
||||||
|
from corporate.views.sponsorship import (
|
||||||
|
remote_realm_sponsorship,
|
||||||
|
remote_realm_sponsorship_page,
|
||||||
|
remote_server_sponsorship,
|
||||||
|
remote_server_sponsorship_page,
|
||||||
|
sponsorship,
|
||||||
|
sponsorship_page,
|
||||||
|
)
|
||||||
from corporate.views.support import support_request
|
from corporate.views.support import support_request
|
||||||
from corporate.views.upgrade import remote_realm_upgrade_page, sponsorship, upgrade, upgrade_page
|
from corporate.views.upgrade import remote_realm_upgrade_page, upgrade, upgrade_page
|
||||||
from corporate.views.webhook import stripe_webhook
|
from corporate.views.webhook import stripe_webhook
|
||||||
from zerver.lib.rest import rest_path
|
from zerver.lib.rest import rest_path
|
||||||
from zerver.lib.url_redirects import LANDING_PAGE_REDIRECTS
|
from zerver.lib.url_redirects import LANDING_PAGE_REDIRECTS
|
||||||
|
@ -40,7 +48,7 @@ i18n_urlpatterns: Any = [
|
||||||
path("jobs/", TemplateView.as_view(template_name="corporate/jobs.html")),
|
path("jobs/", TemplateView.as_view(template_name="corporate/jobs.html")),
|
||||||
# Billing
|
# Billing
|
||||||
path("billing/", billing_home, name="billing_home"),
|
path("billing/", billing_home, name="billing_home"),
|
||||||
path("sponsorship/", sponsorship_request, name="sponsorship_request"),
|
path("sponsorship/", sponsorship_page, name="sponsorship_request"),
|
||||||
path("upgrade/", upgrade_page, name="upgrade_page"),
|
path("upgrade/", upgrade_page, name="upgrade_page"),
|
||||||
path("support/", support_request),
|
path("support/", support_request),
|
||||||
path("billing/event_status/", event_status_page, name="event_status_page"),
|
path("billing/event_status/", event_status_page, name="event_status_page"),
|
||||||
|
@ -151,16 +159,11 @@ i18n_urlpatterns += landing_page_urls
|
||||||
# Make a copy of i18n_urlpatterns so that they appear without prefix for English
|
# Make a copy of i18n_urlpatterns so that they appear without prefix for English
|
||||||
urlpatterns = list(i18n_urlpatterns)
|
urlpatterns = list(i18n_urlpatterns)
|
||||||
|
|
||||||
urlpatterns += [
|
|
||||||
path("api/v1/", include(v1_api_and_json_patterns)),
|
|
||||||
path("json/", include(v1_api_and_json_patterns)),
|
|
||||||
]
|
|
||||||
|
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
path(
|
path(
|
||||||
"remote-billing-login/<signed_billing_access_token>", remote_server_billing_finalize_login
|
"remote-billing-login/<signed_billing_access_token>", remote_server_billing_finalize_login
|
||||||
),
|
),
|
||||||
# Remote server billling endpoints.
|
# Remote server billing endpoints.
|
||||||
path("realm/<realm_uuid>/plans", remote_billing_plans_realm, name="remote_billing_plans_realm"),
|
path("realm/<realm_uuid>/plans", remote_billing_plans_realm, name="remote_billing_plans_realm"),
|
||||||
path(
|
path(
|
||||||
"server/<server_uuid>/plans",
|
"server/<server_uuid>/plans",
|
||||||
|
@ -170,9 +173,27 @@ urlpatterns += [
|
||||||
path("realm/<realm_uuid>/billing", remote_billing_page_realm, name="remote_billing_page_realm"),
|
path("realm/<realm_uuid>/billing", remote_billing_page_realm, name="remote_billing_page_realm"),
|
||||||
path("server/<server_uuid>/", remote_billing_page_server, name="remote_billing_page_server"),
|
path("server/<server_uuid>/", remote_billing_page_server, name="remote_billing_page_server"),
|
||||||
path("realm/<realm_uuid>/upgrade", remote_realm_upgrade_page, name="remote_realm_upgrade_page"),
|
path("realm/<realm_uuid>/upgrade", remote_realm_upgrade_page, name="remote_realm_upgrade_page"),
|
||||||
|
path(
|
||||||
|
"realm/<realm_uuid>/sponsorship",
|
||||||
|
remote_realm_sponsorship_page,
|
||||||
|
name="remote_realm_sponsorship_page",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"server/<server_uuid>/sponsorship",
|
||||||
|
remote_server_sponsorship_page,
|
||||||
|
name="remote_server_sponsorship_page",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"serverlogin/",
|
"serverlogin/",
|
||||||
remote_billing_legacy_server_login,
|
remote_billing_legacy_server_login,
|
||||||
name="remote_billing_legacy_server_login",
|
name="remote_billing_legacy_server_login",
|
||||||
),
|
),
|
||||||
|
# Remote variants of above API endpoints.
|
||||||
|
path("json/realm/<realm_uuid>/sponsorship", remote_realm_sponsorship),
|
||||||
|
path("json/server/<server_uuid>/sponsorship", remote_server_sponsorship),
|
||||||
|
]
|
||||||
|
|
||||||
|
urlpatterns += [
|
||||||
|
path("api/v1/", include(v1_api_and_json_patterns)),
|
||||||
|
path("json/", include(v1_api_and_json_patterns)),
|
||||||
]
|
]
|
||||||
|
|
|
@ -16,19 +16,6 @@ from zerver.models import UserProfile
|
||||||
billing_logger = logging.getLogger("corporate.stripe")
|
billing_logger = logging.getLogger("corporate.stripe")
|
||||||
|
|
||||||
|
|
||||||
@zulip_login_required
|
|
||||||
def sponsorship_request(request: HttpRequest) -> HttpResponse:
|
|
||||||
user = request.user
|
|
||||||
assert user.is_authenticated
|
|
||||||
|
|
||||||
billing_session = RealmBillingSession(user)
|
|
||||||
context = billing_session.get_sponsorship_request_context()
|
|
||||||
if context is None:
|
|
||||||
return HttpResponseRedirect(reverse("billing_home"))
|
|
||||||
|
|
||||||
return render(request, "corporate/sponsorship.html", context=context)
|
|
||||||
|
|
||||||
|
|
||||||
@zulip_login_required
|
@zulip_login_required
|
||||||
@has_request_variables
|
@has_request_variables
|
||||||
def billing_home(
|
def billing_home(
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from corporate.lib.decorator import (
|
||||||
|
authenticated_remote_realm_management_endpoint,
|
||||||
|
authenticated_remote_server_management_endpoint,
|
||||||
|
)
|
||||||
|
from corporate.lib.stripe import (
|
||||||
|
RealmBillingSession,
|
||||||
|
RemoteRealmBillingSession,
|
||||||
|
RemoteServerBillingSession,
|
||||||
|
SponsorshipRequestForm,
|
||||||
|
)
|
||||||
|
from zerver.decorator import require_organization_member, zulip_login_required
|
||||||
|
from zerver.lib.response import json_success
|
||||||
|
from zerver.models import UserProfile
|
||||||
|
from zilencer.models import RemoteRealm, RemoteZulipServer
|
||||||
|
|
||||||
|
|
||||||
|
@zulip_login_required
|
||||||
|
def sponsorship_page(request: HttpRequest) -> HttpResponse:
|
||||||
|
user = request.user
|
||||||
|
assert user.is_authenticated
|
||||||
|
|
||||||
|
billing_session = RealmBillingSession(user)
|
||||||
|
context = billing_session.get_sponsorship_request_context()
|
||||||
|
if context is None:
|
||||||
|
return HttpResponseRedirect(reverse("billing_home"))
|
||||||
|
|
||||||
|
return render(request, "corporate/sponsorship.html", context=context)
|
||||||
|
|
||||||
|
|
||||||
|
@authenticated_remote_realm_management_endpoint
|
||||||
|
def remote_realm_sponsorship_page(
|
||||||
|
request: HttpRequest,
|
||||||
|
remote_realm: RemoteRealm,
|
||||||
|
) -> HttpResponse: # nocoverage
|
||||||
|
billing_session = RemoteRealmBillingSession(remote_realm)
|
||||||
|
context = billing_session.get_sponsorship_request_context()
|
||||||
|
if context is None:
|
||||||
|
return HttpResponseRedirect(reverse("remote_billing_page_realm"))
|
||||||
|
|
||||||
|
return render(request, "corporate/sponsorship.html", context=context)
|
||||||
|
|
||||||
|
|
||||||
|
@authenticated_remote_server_management_endpoint
|
||||||
|
def remote_server_sponsorship_page(
|
||||||
|
request: HttpRequest,
|
||||||
|
remote_server: RemoteZulipServer,
|
||||||
|
) -> HttpResponse: # nocoverage
|
||||||
|
billing_session = RemoteServerBillingSession(remote_server)
|
||||||
|
context = billing_session.get_sponsorship_request_context()
|
||||||
|
if context is None:
|
||||||
|
return HttpResponseRedirect(reverse("remote_billing_page_server"))
|
||||||
|
|
||||||
|
return render(request, "corporate/sponsorship.html", context=context)
|
||||||
|
|
||||||
|
|
||||||
|
@require_organization_member
|
||||||
|
def sponsorship(
|
||||||
|
request: HttpRequest,
|
||||||
|
user: UserProfile,
|
||||||
|
) -> HttpResponse:
|
||||||
|
billing_session = RealmBillingSession(user)
|
||||||
|
post_data = request.POST.copy()
|
||||||
|
form = SponsorshipRequestForm(post_data)
|
||||||
|
billing_session.request_sponsorship(form)
|
||||||
|
return json_success(request)
|
||||||
|
|
||||||
|
|
||||||
|
@authenticated_remote_realm_management_endpoint
|
||||||
|
def remote_realm_sponsorship(
|
||||||
|
request: HttpRequest,
|
||||||
|
remote_realm: RemoteRealm,
|
||||||
|
) -> HttpResponse: # nocoverage
|
||||||
|
billing_session = RemoteRealmBillingSession(remote_realm)
|
||||||
|
post_data = request.POST.copy()
|
||||||
|
form = SponsorshipRequestForm(post_data)
|
||||||
|
billing_session.request_sponsorship(form)
|
||||||
|
return json_success(request)
|
||||||
|
|
||||||
|
|
||||||
|
@authenticated_remote_server_management_endpoint
|
||||||
|
def remote_server_sponsorship(
|
||||||
|
request: HttpRequest,
|
||||||
|
remote_server: RemoteZulipServer,
|
||||||
|
) -> HttpResponse: # nocoverage
|
||||||
|
billing_session = RemoteServerBillingSession(remote_server)
|
||||||
|
post_data = request.POST.copy()
|
||||||
|
form = SponsorshipRequestForm(post_data)
|
||||||
|
billing_session.request_sponsorship(form)
|
||||||
|
return json_success(request)
|
|
@ -1,9 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from django import forms
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import transaction
|
|
||||||
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from pydantic import Json
|
from pydantic import Json
|
||||||
|
@ -19,16 +17,13 @@ from corporate.lib.stripe import (
|
||||||
RemoteRealmBillingSession,
|
RemoteRealmBillingSession,
|
||||||
UpgradeRequest,
|
UpgradeRequest,
|
||||||
)
|
)
|
||||||
from corporate.lib.support import get_support_url
|
from corporate.models import CustomerPlan
|
||||||
from corporate.models import CustomerPlan, ZulipSponsorshipRequest
|
|
||||||
from zerver.actions.users import do_change_is_billing_admin
|
|
||||||
from zerver.decorator import require_organization_member, zulip_login_required
|
from zerver.decorator import require_organization_member, 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_success
|
from zerver.lib.response import json_success
|
||||||
from zerver.lib.send_email import FromAddress, send_email
|
|
||||||
from zerver.lib.typed_endpoint import PathOnly, typed_endpoint
|
from zerver.lib.typed_endpoint import PathOnly, typed_endpoint
|
||||||
from zerver.lib.validator import check_bool, check_int, check_string_in
|
from zerver.lib.validator import check_bool, check_int, check_string_in
|
||||||
from zerver.models import UserProfile, get_org_type_display_name
|
from zerver.models import UserProfile
|
||||||
from zilencer.models import RemoteRealm
|
from zilencer.models import RemoteRealm
|
||||||
|
|
||||||
billing_logger = logging.getLogger("corporate.stripe")
|
billing_logger = logging.getLogger("corporate.stripe")
|
||||||
|
@ -128,84 +123,3 @@ def remote_realm_upgrade_page(
|
||||||
|
|
||||||
response = render(request, "corporate/upgrade.html", context=context)
|
response = render(request, "corporate/upgrade.html", context=context)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class SponsorshipRequestForm(forms.Form):
|
|
||||||
website = forms.URLField(max_length=ZulipSponsorshipRequest.MAX_ORG_URL_LENGTH, required=False)
|
|
||||||
organization_type = forms.IntegerField()
|
|
||||||
description = forms.CharField(widget=forms.Textarea)
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@require_organization_member
|
|
||||||
def sponsorship(
|
|
||||||
request: HttpRequest,
|
|
||||||
user: UserProfile,
|
|
||||||
) -> HttpResponse:
|
|
||||||
realm = user.realm
|
|
||||||
billing_session = RealmBillingSession(user)
|
|
||||||
|
|
||||||
requested_by = user.full_name
|
|
||||||
user_role = user.get_role_name()
|
|
||||||
support_url = get_support_url(realm)
|
|
||||||
|
|
||||||
post_data = request.POST.copy()
|
|
||||||
form = SponsorshipRequestForm(post_data)
|
|
||||||
|
|
||||||
if form.is_valid():
|
|
||||||
with transaction.atomic():
|
|
||||||
# Ensures customer is created first before updating sponsorship status.
|
|
||||||
billing_session.update_customer_sponsorship_status(True)
|
|
||||||
sponsorship_request = ZulipSponsorshipRequest(
|
|
||||||
customer=billing_session.get_customer(),
|
|
||||||
requested_by=user,
|
|
||||||
org_website=form.cleaned_data["website"],
|
|
||||||
org_description=form.cleaned_data["description"],
|
|
||||||
org_type=form.cleaned_data["organization_type"],
|
|
||||||
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"],
|
|
||||||
)
|
|
||||||
sponsorship_request.save()
|
|
||||||
|
|
||||||
org_type = form.cleaned_data["organization_type"]
|
|
||||||
if realm.org_type != org_type:
|
|
||||||
realm.org_type = org_type
|
|
||||||
realm.save(update_fields=["org_type"])
|
|
||||||
|
|
||||||
do_change_is_billing_admin(user, True)
|
|
||||||
|
|
||||||
org_type_display_name = get_org_type_display_name(org_type)
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"requested_by": requested_by,
|
|
||||||
"user_role": user_role,
|
|
||||||
"string_id": realm.string_id,
|
|
||||||
"support_url": support_url,
|
|
||||||
"organization_type": org_type_display_name,
|
|
||||||
"website": sponsorship_request.org_website,
|
|
||||||
"description": sponsorship_request.org_description,
|
|
||||||
"expected_total_users": sponsorship_request.expected_total_users,
|
|
||||||
"paid_users_count": sponsorship_request.paid_users_count,
|
|
||||||
"paid_users_description": sponsorship_request.paid_users_description,
|
|
||||||
}
|
|
||||||
# Sent to the server's support team, so this email is not user-facing.
|
|
||||||
send_email(
|
|
||||||
"zerver/emails/sponsorship_request",
|
|
||||||
to_emails=[FromAddress.SUPPORT],
|
|
||||||
from_name="Zulip sponsorship request",
|
|
||||||
from_address=FromAddress.tokenized_no_reply_address(),
|
|
||||||
reply_to_email=user.delivery_email,
|
|
||||||
context=context,
|
|
||||||
)
|
|
||||||
|
|
||||||
return json_success(request)
|
|
||||||
else:
|
|
||||||
message = " ".join(
|
|
||||||
error["message"]
|
|
||||||
for error_list in form.errors.get_json_data().values()
|
|
||||||
for error in error_list
|
|
||||||
)
|
|
||||||
raise BillingError("Form validation error", message=message)
|
|
||||||
|
|
|
@ -55,6 +55,7 @@ function create_ajax_request(): void {
|
||||||
|
|
||||||
void $.ajax({
|
void $.ajax({
|
||||||
type: "post",
|
type: "post",
|
||||||
|
// TODO: This needs to be conditional on billing session type
|
||||||
url: "/json/billing/sponsorship",
|
url: "/json/billing/sponsorship",
|
||||||
data,
|
data,
|
||||||
success() {
|
success() {
|
||||||
|
|
Loading…
Reference in New Issue