2018-09-25 12:24:11 +02:00
|
|
|
import logging
|
2020-03-23 13:35:04 +01:00
|
|
|
from decimal import Decimal
|
2018-09-08 00:49:54 +02:00
|
|
|
import stripe
|
2019-07-28 23:51:27 +02:00
|
|
|
from typing import Any, Dict, cast, Optional, Union
|
2018-09-25 12:24:11 +02:00
|
|
|
|
|
|
|
from django.core import signing
|
|
|
|
from django.http import HttpRequest, HttpResponse, HttpResponseRedirect
|
2018-12-28 07:20:30 +01:00
|
|
|
from django.utils.timezone import now as timezone_now
|
2019-02-02 23:53:22 +01:00
|
|
|
from django.utils.translation import ugettext as _
|
|
|
|
from django.shortcuts import render
|
2018-09-25 12:24:11 +02:00
|
|
|
from django.urls import reverse
|
|
|
|
from django.conf import settings
|
|
|
|
|
2018-11-01 11:26:29 +01:00
|
|
|
from zerver.decorator import zulip_login_required, require_billing_access
|
2018-09-25 12:24:11 +02:00
|
|
|
from zerver.lib.request import REQ, has_request_variables
|
|
|
|
from zerver.lib.response import json_error, json_success
|
2019-02-02 23:53:22 +01:00
|
|
|
from zerver.lib.validator import check_string, check_int
|
|
|
|
from zerver.models import UserProfile
|
2018-09-25 12:33:30 +02:00
|
|
|
from corporate.lib.stripe import STRIPE_PUBLISHABLE_KEY, \
|
2019-10-07 19:21:29 +02:00
|
|
|
stripe_get_customer, get_latest_seat_count, \
|
2018-12-15 09:33:25 +01:00
|
|
|
process_initial_upgrade, sign_string, \
|
2019-04-08 05:16:35 +02:00
|
|
|
unsign_string, BillingError, do_change_plan_status, do_replace_payment_source, \
|
2020-05-08 12:43:52 +02:00
|
|
|
MIN_INVOICED_LICENSES, MAX_INVOICED_LICENSES, DEFAULT_INVOICE_DAYS_UNTIL_DUE, \
|
2019-04-10 09:14:20 +02:00
|
|
|
start_of_next_billing_cycle, renewal_amount, \
|
2020-04-23 20:10:15 +02:00
|
|
|
make_end_of_cycle_updates_if_needed, downgrade_now
|
2020-03-24 14:14:03 +01:00
|
|
|
from corporate.models import CustomerPlan, get_current_plan_by_customer, \
|
2020-03-24 14:22:27 +01:00
|
|
|
get_customer_by_realm, get_current_plan_by_realm
|
2018-09-25 12:24:11 +02:00
|
|
|
|
|
|
|
billing_logger = logging.getLogger('corporate.stripe')
|
|
|
|
|
2018-12-22 05:29:25 +01:00
|
|
|
def unsign_seat_count(signed_seat_count: str, salt: str) -> int:
|
2018-09-25 12:24:11 +02:00
|
|
|
try:
|
2018-12-22 05:29:25 +01:00
|
|
|
return int(unsign_string(signed_seat_count, salt))
|
2018-09-25 12:24:11 +02:00
|
|
|
except signing.BadSignature:
|
2018-12-22 05:29:25 +01:00
|
|
|
raise BillingError('tampered seat count')
|
|
|
|
|
|
|
|
def check_upgrade_parameters(
|
2019-11-13 08:17:49 +01:00
|
|
|
billing_modality: str, schedule: str, license_management: Optional[str], licenses: Optional[int],
|
2018-12-22 05:29:25 +01:00
|
|
|
has_stripe_token: bool, seat_count: int) -> None:
|
|
|
|
if billing_modality not in ['send_invoice', 'charge_automatically']:
|
|
|
|
raise BillingError('unknown billing_modality')
|
|
|
|
if schedule not in ['annual', 'monthly']:
|
|
|
|
raise BillingError('unknown schedule')
|
2019-01-29 06:41:12 +01:00
|
|
|
if license_management not in ['automatic', 'manual']:
|
2018-12-22 05:29:25 +01:00
|
|
|
raise BillingError('unknown license_management')
|
|
|
|
|
|
|
|
if billing_modality == 'charge_automatically':
|
|
|
|
if not has_stripe_token:
|
|
|
|
raise BillingError('autopay with no card')
|
|
|
|
|
|
|
|
min_licenses = seat_count
|
2020-05-08 12:43:52 +02:00
|
|
|
max_licenses = None
|
2018-12-22 05:29:25 +01:00
|
|
|
if billing_modality == 'send_invoice':
|
|
|
|
min_licenses = max(seat_count, MIN_INVOICED_LICENSES)
|
2020-05-08 12:43:52 +02:00
|
|
|
max_licenses = MAX_INVOICED_LICENSES
|
|
|
|
|
2018-12-22 05:29:25 +01:00
|
|
|
if licenses is None or licenses < min_licenses:
|
|
|
|
raise BillingError('not enough licenses',
|
2019-04-20 03:49:03 +02:00
|
|
|
_("You must invoice for at least {} users.").format(min_licenses))
|
2018-09-25 12:24:11 +02:00
|
|
|
|
2020-05-08 12:43:52 +02:00
|
|
|
if max_licenses is not None and licenses > max_licenses:
|
|
|
|
message = _("Invoices with more than {} licenses can't be processed from this page. To complete "
|
|
|
|
"the upgrade, please contact {}.").format(max_licenses, settings.ZULIP_ADMINISTRATOR)
|
|
|
|
raise BillingError('too many licenses', message)
|
|
|
|
|
2018-12-23 09:10:57 +01:00
|
|
|
# Should only be called if the customer is being charged automatically
|
|
|
|
def payment_method_string(stripe_customer: stripe.Customer) -> str:
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
stripe_source: Optional[Union[stripe.Card, stripe.Source]] = stripe_customer.default_source
|
2018-09-08 00:49:54 +02:00
|
|
|
# In case of e.g. an expired card
|
|
|
|
if stripe_source is None: # nocoverage
|
|
|
|
return _("No payment method on file")
|
|
|
|
if stripe_source.object == "card":
|
2019-04-20 03:49:03 +02:00
|
|
|
return _("%(brand)s ending in %(last4)s") % {
|
2018-12-23 09:10:57 +01:00
|
|
|
'brand': cast(stripe.Card, stripe_source).brand,
|
2019-04-20 03:49:03 +02:00
|
|
|
'last4': cast(stripe.Card, stripe_source).last4}
|
2018-12-23 09:10:57 +01:00
|
|
|
# There might be one-off stuff we do for a particular customer that
|
|
|
|
# would land them here. E.g. by default we don't support ACH for
|
|
|
|
# automatic payments, but in theory we could add it for a customer via
|
|
|
|
# the Stripe dashboard.
|
2019-04-20 03:49:03 +02:00
|
|
|
return _("Unknown payment method. Please contact %s.") % (settings.ZULIP_ADMINISTRATOR,) # nocoverage
|
2018-09-08 00:49:54 +02:00
|
|
|
|
2018-12-07 18:43:22 +01:00
|
|
|
@has_request_variables
|
|
|
|
def upgrade(request: HttpRequest, user: UserProfile,
|
2018-12-22 05:29:25 +01:00
|
|
|
billing_modality: str=REQ(validator=check_string),
|
2018-12-12 23:23:15 +01:00
|
|
|
schedule: str=REQ(validator=check_string),
|
2019-11-13 08:17:49 +01:00
|
|
|
license_management: Optional[str]=REQ(validator=check_string, default=None),
|
|
|
|
licenses: Optional[int]=REQ(validator=check_int, default=None),
|
|
|
|
stripe_token: Optional[str]=REQ(validator=check_string, default=None),
|
2018-12-22 05:29:25 +01:00
|
|
|
signed_seat_count: str=REQ(validator=check_string),
|
|
|
|
salt: str=REQ(validator=check_string)) -> HttpResponse:
|
2018-12-07 18:43:22 +01:00
|
|
|
try:
|
2018-12-22 05:29:25 +01:00
|
|
|
seat_count = unsign_seat_count(signed_seat_count, salt)
|
|
|
|
if billing_modality == 'charge_automatically' and license_management == 'automatic':
|
2018-12-22 01:43:44 +01:00
|
|
|
licenses = seat_count
|
2018-12-22 05:29:25 +01:00
|
|
|
if billing_modality == 'send_invoice':
|
|
|
|
schedule = 'annual'
|
|
|
|
license_management = 'manual'
|
|
|
|
check_upgrade_parameters(
|
|
|
|
billing_modality, schedule, license_management, licenses,
|
|
|
|
stripe_token is not None, seat_count)
|
2019-11-13 08:17:49 +01:00
|
|
|
assert licenses is not None
|
2019-01-29 06:41:12 +01:00
|
|
|
automanage_licenses = license_management == 'automatic'
|
2018-12-22 05:29:25 +01:00
|
|
|
|
|
|
|
billing_schedule = {'annual': CustomerPlan.ANNUAL,
|
|
|
|
'monthly': CustomerPlan.MONTHLY}[schedule]
|
2018-12-15 09:33:25 +01:00
|
|
|
process_initial_upgrade(user, licenses, automanage_licenses, billing_schedule, stripe_token)
|
2018-12-07 18:43:22 +01:00
|
|
|
except BillingError as e:
|
2018-12-24 00:35:48 +01:00
|
|
|
if not settings.TEST_SUITE: # nocoverage
|
2019-01-29 16:46:10 +01:00
|
|
|
billing_logger.warning(
|
2020-05-02 20:57:12 +02:00
|
|
|
"BillingError during upgrade: %s. user=%s, realm=%s (%s), billing_modality=%s, "
|
|
|
|
"schedule=%s, license_management=%s, licenses=%s, has stripe_token: %s",
|
|
|
|
e.description, user.id, user.realm.id, user.realm.string_id, billing_modality,
|
|
|
|
schedule, license_management, licenses, stripe_token is not None,
|
|
|
|
)
|
2018-12-07 18:43:22 +01:00
|
|
|
return json_error(e.message, data={'error_description': e.description})
|
|
|
|
except Exception as e:
|
|
|
|
billing_logger.exception("Uncaught exception in billing: %s" % (e,))
|
|
|
|
error_message = BillingError.CONTACT_SUPPORT
|
|
|
|
error_description = "uncaught exception during upgrade"
|
|
|
|
return json_error(error_message, data={'error_description': error_description})
|
|
|
|
else:
|
|
|
|
return json_success()
|
|
|
|
|
2018-09-25 12:24:11 +02:00
|
|
|
@zulip_login_required
|
|
|
|
def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
|
|
|
if not settings.BILLING_ENABLED:
|
|
|
|
return render(request, "404.html")
|
|
|
|
|
|
|
|
user = request.user
|
2020-03-23 13:35:04 +01:00
|
|
|
customer = get_customer_by_realm(user.realm)
|
2020-03-24 14:14:03 +01:00
|
|
|
if customer is not None and get_current_plan_by_customer(customer) is not None:
|
2018-09-25 12:24:11 +02:00
|
|
|
return HttpResponseRedirect(reverse('corporate.views.billing_home'))
|
|
|
|
|
2020-03-23 13:35:04 +01:00
|
|
|
percent_off = Decimal(0)
|
2018-12-12 19:41:03 +01:00
|
|
|
if customer is not None and customer.default_discount is not None:
|
|
|
|
percent_off = customer.default_discount
|
2018-11-30 02:27:01 +01:00
|
|
|
|
2019-10-07 19:21:29 +02:00
|
|
|
seat_count = get_latest_seat_count(user.realm)
|
2018-09-25 12:24:11 +02:00
|
|
|
signed_seat_count, salt = sign_string(str(seat_count))
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
context: Dict[str, Any] = {
|
2018-09-25 12:24:11 +02:00
|
|
|
'publishable_key': STRIPE_PUBLISHABLE_KEY,
|
2019-11-19 02:02:57 +01:00
|
|
|
'email': user.delivery_email,
|
2018-09-25 12:24:11 +02:00
|
|
|
'seat_count': seat_count,
|
|
|
|
'signed_seat_count': signed_seat_count,
|
|
|
|
'salt': salt,
|
2018-12-22 01:43:44 +01:00
|
|
|
'min_invoiced_licenses': max(seat_count, MIN_INVOICED_LICENSES),
|
2018-11-18 10:18:14 +01:00
|
|
|
'default_invoice_days_until_due': DEFAULT_INVOICE_DAYS_UNTIL_DUE,
|
2018-10-24 06:09:01 +02:00
|
|
|
'plan': "Zulip Standard",
|
2020-04-23 20:10:15 +02:00
|
|
|
"free_trial_months": settings.FREE_TRIAL_MONTHS,
|
2019-09-13 01:15:53 +02:00
|
|
|
'page_params': {
|
2018-11-29 08:51:53 +01:00
|
|
|
'seat_count': seat_count,
|
|
|
|
'annual_price': 8000,
|
|
|
|
'monthly_price': 800,
|
2018-12-12 19:41:03 +01:00
|
|
|
'percent_off': float(percent_off),
|
2019-09-13 01:15:53 +02:00
|
|
|
},
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
}
|
2018-09-25 12:30:17 +02:00
|
|
|
response = render(request, 'corporate/upgrade.html', context=context)
|
2018-09-25 12:24:11 +02:00
|
|
|
return response
|
|
|
|
|
|
|
|
@zulip_login_required
|
|
|
|
def billing_home(request: HttpRequest) -> HttpResponse:
|
|
|
|
user = request.user
|
2020-03-23 13:35:04 +01:00
|
|
|
customer = get_customer_by_realm(user.realm)
|
2018-09-25 12:24:11 +02:00
|
|
|
if customer is None:
|
|
|
|
return HttpResponseRedirect(reverse('corporate.views.initial_upgrade'))
|
2018-12-15 09:33:25 +01:00
|
|
|
if not CustomerPlan.objects.filter(customer=customer).exists():
|
2018-09-25 12:24:11 +02:00
|
|
|
return HttpResponseRedirect(reverse('corporate.views.initial_upgrade'))
|
|
|
|
|
|
|
|
if not user.is_realm_admin and not user.is_billing_admin:
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
context: Dict[str, Any] = {'admin_access': False}
|
2018-09-25 12:30:17 +02:00
|
|
|
return render(request, 'corporate/billing.html', context=context)
|
2018-09-25 12:24:11 +02:00
|
|
|
|
2020-04-03 16:17:34 +02:00
|
|
|
context = {
|
|
|
|
'admin_access': True,
|
|
|
|
'has_active_plan': False,
|
|
|
|
}
|
2019-04-08 05:16:35 +02:00
|
|
|
|
2020-03-24 14:14:03 +01:00
|
|
|
plan = get_current_plan_by_customer(customer)
|
2018-12-15 09:33:25 +01:00
|
|
|
if plan is not None:
|
2019-01-26 20:45:26 +01:00
|
|
|
now = timezone_now()
|
2019-04-11 00:24:45 +02:00
|
|
|
last_ledger_entry = make_end_of_cycle_updates_if_needed(plan, now)
|
2019-04-08 05:16:35 +02:00
|
|
|
if last_ledger_entry is not None:
|
2020-03-26 15:54:34 +01:00
|
|
|
plan_name = {
|
|
|
|
CustomerPlan.STANDARD: 'Zulip Standard',
|
|
|
|
CustomerPlan.PLUS: 'Zulip Plus',
|
|
|
|
}[plan.tier]
|
2020-04-23 20:10:15 +02:00
|
|
|
free_trial = plan.status == CustomerPlan.FREE_TRIAL
|
2020-04-24 17:38:13 +02:00
|
|
|
downgrade_at_end_of_cycle = plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE
|
2019-04-08 05:16:35 +02:00
|
|
|
licenses = last_ledger_entry.licenses
|
2019-10-07 19:21:29 +02:00
|
|
|
licenses_used = get_latest_seat_count(user.realm)
|
2019-04-08 05:16:35 +02:00
|
|
|
# Should do this in javascript, using the user's timezone
|
|
|
|
renewal_date = '{dt:%B} {dt.day}, {dt.year}'.format(dt=start_of_next_billing_cycle(plan, now))
|
|
|
|
renewal_cents = renewal_amount(plan, now)
|
|
|
|
charge_automatically = plan.charge_automatically
|
2020-03-26 19:08:00 +01:00
|
|
|
stripe_customer = stripe_get_customer(customer.stripe_customer_id)
|
2019-04-08 05:16:35 +02:00
|
|
|
if charge_automatically:
|
|
|
|
payment_method = payment_method_string(stripe_customer)
|
|
|
|
else:
|
|
|
|
payment_method = 'Billed by invoice'
|
2018-09-25 12:24:11 +02:00
|
|
|
|
2020-04-03 16:17:34 +02:00
|
|
|
context.update({
|
|
|
|
'plan_name': plan_name,
|
|
|
|
'has_active_plan': True,
|
2020-04-23 20:10:15 +02:00
|
|
|
'free_trial': free_trial,
|
2020-04-24 17:38:13 +02:00
|
|
|
'downgrade_at_end_of_cycle': downgrade_at_end_of_cycle,
|
2020-04-03 16:17:34 +02:00
|
|
|
'licenses': licenses,
|
|
|
|
'licenses_used': licenses_used,
|
|
|
|
'renewal_date': renewal_date,
|
|
|
|
'renewal_amount': '{:,.2f}'.format(renewal_cents / 100.),
|
|
|
|
'payment_method': payment_method,
|
|
|
|
'charge_automatically': charge_automatically,
|
|
|
|
'publishable_key': STRIPE_PUBLISHABLE_KEY,
|
|
|
|
'stripe_email': stripe_customer.email,
|
2020-04-24 17:38:13 +02:00
|
|
|
'CustomerPlan': CustomerPlan,
|
2020-04-03 16:17:34 +02:00
|
|
|
})
|
|
|
|
|
2018-09-25 12:30:17 +02:00
|
|
|
return render(request, 'corporate/billing.html', context=context)
|
2018-09-25 12:24:11 +02:00
|
|
|
|
2018-11-01 11:26:29 +01:00
|
|
|
@require_billing_access
|
2019-04-08 05:16:35 +02:00
|
|
|
@has_request_variables
|
2020-04-23 20:02:16 +02:00
|
|
|
def change_plan_status(request: HttpRequest, user: UserProfile,
|
|
|
|
status: int=REQ("status", validator=check_int)) -> HttpResponse:
|
2020-04-23 20:10:15 +02:00
|
|
|
assert(status in [CustomerPlan.ACTIVE, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE, CustomerPlan.ENDED])
|
|
|
|
|
2020-03-24 14:22:27 +01:00
|
|
|
plan = get_current_plan_by_realm(user.realm)
|
2019-04-08 05:16:35 +02:00
|
|
|
assert(plan is not None) # for mypy
|
2020-04-23 20:10:15 +02:00
|
|
|
|
|
|
|
if status == CustomerPlan.ACTIVE:
|
|
|
|
assert(plan.status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE)
|
|
|
|
do_change_plan_status(plan, status)
|
|
|
|
elif status == CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE:
|
|
|
|
assert(plan.status == CustomerPlan.ACTIVE)
|
|
|
|
do_change_plan_status(plan, status)
|
|
|
|
elif status == CustomerPlan.ENDED:
|
|
|
|
assert(plan.status == CustomerPlan.FREE_TRIAL)
|
|
|
|
downgrade_now(user.realm)
|
2018-09-25 12:24:11 +02:00
|
|
|
return json_success()
|
|
|
|
|
2018-11-01 11:26:29 +01:00
|
|
|
@require_billing_access
|
2018-09-25 12:24:11 +02:00
|
|
|
@has_request_variables
|
|
|
|
def replace_payment_source(request: HttpRequest, user: UserProfile,
|
|
|
|
stripe_token: str=REQ("stripe_token", validator=check_string)) -> HttpResponse:
|
|
|
|
try:
|
2019-04-04 10:02:49 +02:00
|
|
|
do_replace_payment_source(user, stripe_token, pay_invoices=True)
|
2018-09-25 12:24:11 +02:00
|
|
|
except BillingError as e:
|
|
|
|
return json_error(e.message, data={'error_description': e.description})
|
|
|
|
return json_success()
|