2018-01-30 20:49:25 +01:00
import logging
2019-01-30 19:04:32 +01:00
import math
2018-01-30 20:49:25 +01:00
import os
2020-09-05 04:02:13 +02:00
import secrets
2023-10-26 14:11:43 +02:00
from abc import ABC , abstractmethod
2024-07-12 02:30:25 +02:00
from collections . abc import Callable , Generator
2023-10-31 15:51:51 +01:00
from dataclasses import dataclass
2024-01-08 20:34:16 +01:00
from datetime import datetime , timedelta , timezone
2020-06-11 00:54:34 +02:00
from decimal import Decimal
2024-08-29 20:48:40 +02:00
from enum import Enum , IntEnum
2020-06-11 00:54:34 +02:00
from functools import wraps
2024-07-12 02:30:25 +02:00
from typing import Any , Literal , TypedDict , TypeVar
2023-12-06 15:00:23 +01:00
from urllib . parse import urlencode , urljoin
2018-01-30 20:49:25 +01:00
2020-06-11 00:54:34 +02:00
import stripe
2023-11-30 01:48:46 +01:00
from django import forms
2018-01-30 20:49:25 +01:00
from django . conf import settings
2023-11-14 11:59:48 +01:00
from django . core import signing
2020-06-11 00:54:34 +02:00
from django . core . signing import Signer
2018-06-28 00:48:51 +02:00
from django . db import transaction
2023-12-07 15:27:39 +01:00
from django . http import HttpRequest , HttpResponse
from django . shortcuts import render
2021-06-11 12:53:45 +02:00
from django . urls import reverse
2018-08-14 03:33:31 +02:00
from django . utils . timezone import now as timezone_now
2021-04-16 00:57:30 +02:00
from django . utils . translation import gettext as _
from django . utils . translation import gettext_lazy
2020-07-17 12:56:06 +02:00
from django . utils . translation import override as override_language
2023-10-26 14:11:43 +02:00
from typing_extensions import ParamSpec , override
2018-01-30 20:49:25 +01:00
2020-06-11 00:54:34 +02:00
from corporate . models import (
Customer ,
CustomerPlan ,
2024-01-17 13:55:25 +01:00
CustomerPlanOffer ,
2024-02-02 13:04:41 +01:00
Invoice ,
2020-06-11 00:54:34 +02:00
LicenseLedger ,
2023-11-06 13:52:12 +01:00
Session ,
2023-12-08 08:25:05 +01:00
SponsoredPlanTypes ,
2023-11-30 01:48:46 +01:00
ZulipSponsorshipRequest ,
2020-06-11 00:54:34 +02:00
get_current_plan_by_customer ,
get_current_plan_by_realm ,
get_customer_by_realm ,
2023-11-09 20:40:42 +01:00
get_customer_by_remote_realm ,
get_customer_by_remote_server ,
2020-06-11 00:54:34 +02:00
)
2024-04-14 00:25:05 +02:00
from zerver . lib . cache import cache_with_key , get_realm_seat_count_cache_key
2021-07-04 08:19:18 +02:00
from zerver . lib . exceptions import JsonableError
2018-01-30 20:49:25 +01:00
from zerver . lib . logging_util import log_to_file
2023-11-30 01:48:46 +01:00
from zerver . lib . send_email import (
FromAddress ,
send_email ,
send_email_to_billing_admins_and_realm_owners ,
)
2018-06-28 00:48:51 +02:00
from zerver . lib . timestamp import datetime_to_timestamp , timestamp_to_datetime
2023-12-06 15:00:23 +01:00
from zerver . lib . url_encoding import append_url_query_string
2021-07-25 16:31:12 +02:00
from zerver . lib . utils import assert_is_not_none
2023-12-15 02:14:24 +01:00
from zerver . models import Realm , RealmAuditLog , UserProfile
from zerver . models . realms import get_org_type_display_name , get_realm
2023-12-15 01:16:00 +01:00
from zerver . models . users import get_system_bot
2023-12-06 19:25:49 +01:00
from zilencer . lib . remote_counts import MissingDataError
2023-11-09 20:40:42 +01:00
from zilencer . models import (
RemoteRealm ,
RemoteRealmAuditLog ,
2023-12-07 17:01:29 +01:00
RemoteRealmBillingUser ,
2023-12-08 19:00:04 +01:00
RemoteServerBillingUser ,
2023-11-09 20:40:42 +01:00
RemoteZulipServer ,
RemoteZulipServerAuditLog ,
2023-12-06 14:26:07 +01:00
get_remote_realm_guest_and_non_guest_count ,
2023-12-07 01:39:05 +01:00
get_remote_server_guest_and_non_guest_count ,
2023-12-06 19:25:49 +01:00
has_stale_audit_log ,
2023-11-09 20:40:42 +01:00
)
2019-11-13 01:11:56 +01:00
from zproject . config import get_secret
2018-01-30 20:49:25 +01:00
2021-02-12 08:20:45 +01:00
stripe . api_key = get_secret ( " stripe_secret_key " )
2018-01-30 20:49:25 +01:00
2021-02-12 08:19:30 +01:00
BILLING_LOG_PATH = os . path . join (
2021-02-12 08:20:45 +01:00
" /var/log/zulip " if not settings . DEVELOPMENT else settings . DEVELOPMENT_LOG_DIRECTORY ,
" billing.log " ,
2021-02-12 08:19:30 +01:00
)
2021-02-12 08:20:45 +01:00
billing_logger = logging . getLogger ( " corporate.stripe " )
2018-01-30 20:49:25 +01:00
log_to_file ( billing_logger , BILLING_LOG_PATH )
2021-02-12 08:20:45 +01:00
log_to_file ( logging . getLogger ( " stripe " ) , BILLING_LOG_PATH )
2018-01-30 20:49:25 +01:00
2022-04-13 16:42:42 +02:00
ParamT = ParamSpec ( " ParamT " )
ReturnT = TypeVar ( " ReturnT " )
2018-01-30 21:03:59 +01:00
2023-12-14 13:45:31 +01:00
BILLING_SUPPORT_EMAIL = " sales@zulip.com "
2018-12-22 01:43:44 +01:00
MIN_INVOICED_LICENSES = 30
2020-05-08 12:43:52 +02:00
MAX_INVOICED_LICENSES = 1000
2024-03-04 00:44:59 +01:00
DEFAULT_INVOICE_DAYS_UNTIL_DUE = 15
2018-09-08 00:49:54 +02:00
2023-11-14 11:59:48 +01:00
VALID_BILLING_MODALITY_VALUES = [ " send_invoice " , " charge_automatically " ]
VALID_BILLING_SCHEDULE_VALUES = [ " annual " , " monthly " ]
VALID_LICENSE_MANAGEMENT_VALUES = [ " automatic " , " manual " ]
2023-11-16 16:14:43 +01:00
CARD_CAPITALIZATION = {
" amex " : " American Express " ,
" diners " : " Diners Club " ,
" discover " : " Discover " ,
" jcb " : " JCB " ,
" mastercard " : " Mastercard " ,
" unionpay " : " UnionPay " ,
" visa " : " Visa " ,
}
2021-09-07 13:59:44 +02:00
# The version of Stripe API the billing system supports.
STRIPE_API_VERSION = " 2020-08-27 "
2023-10-10 22:52:17 +02:00
stripe . api_version = STRIPE_API_VERSION
2021-02-12 08:19:30 +01:00
2023-11-14 03:20:29 +01:00
# This function imitates the behavior of the format_money in billing/helpers.ts
def format_money ( cents : float ) - > str :
# allow for small floating point errors
cents = math . ceil ( cents - 0.001 )
if cents % 100 == 0 :
precision = 0
else :
precision = 2
dollars = cents / 100
# Format the number as a string with the correct number of decimal places
return f " { dollars : . { precision } f } "
2024-01-17 13:55:25 +01:00
def get_amount_due_fixed_price_plan ( fixed_price : int , billing_schedule : int ) - > int :
amount_due = fixed_price
if billing_schedule == CustomerPlan . BILLING_SCHEDULE_MONTHLY :
amount_due = int ( float ( format_money ( fixed_price / 12 ) ) * 100 )
return amount_due
2024-07-12 02:30:23 +02:00
def format_discount_percentage ( discount : Decimal | None ) - > str | None :
2023-11-24 07:34:24 +01:00
if type ( discount ) is not Decimal or discount == Decimal ( 0 ) :
2023-11-23 18:05:06 +01:00
return None
2024-01-04 15:59:36 +01:00
# This will look good for any custom discounts that we apply.
2023-11-23 18:05:06 +01:00
if discount * 100 % 100 == 0 :
precision = 0
else :
precision = 2 # nocoverage
return f " { discount : . { precision } f } "
2019-10-07 19:21:29 +02:00
def get_latest_seat_count ( realm : Realm ) - > int :
2022-08-14 18:41:59 +02:00
return get_seat_count ( realm , extra_non_guests_count = 0 , extra_guests_count = 0 )
2024-04-14 00:25:05 +02:00
@cache_with_key ( lambda realm : get_realm_seat_count_cache_key ( realm . id ) , timeout = 3600 * 24 )
def get_cached_seat_count ( realm : Realm ) - > int :
# This is a cache value we're intentionally okay with not invalidating.
# All that means is that this value will lag up to 24 hours before getting updated.
# We use this for calculating the uploaded files storage limit for paid Cloud organizations.
return get_latest_seat_count ( realm )
2024-08-26 18:01:20 +02:00
def get_non_guest_user_count ( realm : Realm ) - > int :
return (
2021-02-12 08:19:30 +01:00
UserProfile . objects . filter ( realm = realm , is_active = True , is_bot = False )
. exclude ( role = UserProfile . ROLE_GUEST )
. count ( )
)
2022-08-01 10:44:41 +02:00
2024-08-26 18:01:20 +02:00
def get_guest_user_count ( realm : Realm ) - > int :
# Same query to get guest user count as in render_stats in analytics/views/stats.py.
return UserProfile . objects . filter (
realm = realm , is_active = True , is_bot = False , role = UserProfile . ROLE_GUEST
) . count ( )
def get_seat_count (
realm : Realm , extra_non_guests_count : int = 0 , extra_guests_count : int = 0
) - > int :
non_guests = get_non_guest_user_count ( realm ) + extra_non_guests_count
guests = get_guest_user_count ( realm ) + extra_guests_count
2022-08-01 10:44:41 +02:00
# This formula achieves the pricing of the first 5*N guests
# being free of charge (where N is the number of non-guests in the organization)
# and each consecutive one being worth 1/5 the non-guest price.
2019-01-30 19:04:32 +01:00
return max ( non_guests , math . ceil ( guests / 5 ) )
2018-03-31 04:13:44 +02:00
2021-02-12 08:19:30 +01:00
2024-07-12 02:30:17 +02:00
def sign_string ( string : str ) - > tuple [ str , str ] :
2020-09-05 04:02:13 +02:00
salt = secrets . token_hex ( 32 )
2018-07-13 17:34:39 +02:00
signer = Signer ( salt = salt )
return signer . sign ( string ) , salt
2021-02-12 08:19:30 +01:00
2018-07-13 17:34:39 +02:00
def unsign_string ( signed_string : str , salt : str ) - > str :
signer = Signer ( salt = salt )
return signer . unsign ( signed_string )
2021-02-12 08:19:30 +01:00
2023-11-14 11:59:48 +01:00
def unsign_seat_count ( signed_seat_count : str , salt : str ) - > int :
try :
return int ( unsign_string ( signed_seat_count , salt ) )
except signing . BadSignature :
raise BillingError ( " tampered seat count " )
2023-04-10 21:48:52 +02:00
def validate_licenses (
charge_automatically : bool ,
2024-07-12 02:30:23 +02:00
licenses : int | None ,
2023-04-10 21:48:52 +02:00
seat_count : int ,
exempt_from_license_number_check : bool ,
2023-12-09 08:16:53 +01:00
min_licenses_for_plan : int ,
2023-04-10 21:48:52 +02:00
) - > None :
2023-12-09 08:16:53 +01:00
min_licenses = max ( seat_count , min_licenses_for_plan )
2020-12-17 16:33:19 +01:00
max_licenses = None
2024-03-04 00:44:59 +01:00
# max / min license check for invoiced plans is disabled in production right now.
# Logic and tests are kept in case we decide to enable it in future.
if settings . TEST_SUITE and not charge_automatically :
2020-12-17 16:33:19 +01:00
min_licenses = max ( seat_count , MIN_INVOICED_LICENSES )
max_licenses = MAX_INVOICED_LICENSES
2023-04-10 21:48:52 +02:00
if licenses is None or ( not exempt_from_license_number_check and licenses < min_licenses ) :
2020-12-17 16:33:19 +01:00
raise BillingError (
2023-07-17 22:40:33 +02:00
" not enough licenses " ,
2023-11-12 04:13:36 +01:00
_ (
" You must purchase licenses for all active users in your organization (minimum {min_licenses} ). "
) . format ( min_licenses = min_licenses ) ,
2020-12-17 16:33:19 +01:00
)
if max_licenses is not None and licenses > max_licenses :
message = _ (
2023-07-17 22:40:33 +02:00
" Invoices with more than {max_licenses} licenses can ' t be processed from this page. To "
" complete the upgrade, please contact {email} . "
) . format ( max_licenses = max_licenses , email = settings . ZULIP_ADMINISTRATOR )
2020-12-17 16:33:19 +01:00
raise BillingError ( " too many licenses " , message )
2023-11-14 11:59:48 +01:00
def check_upgrade_parameters (
billing_modality : str ,
schedule : str ,
2024-07-12 02:30:23 +02:00
license_management : str | None ,
licenses : int | None ,
2023-11-14 11:59:48 +01:00
seat_count : int ,
exempt_from_license_number_check : bool ,
2023-12-09 08:16:53 +01:00
min_licenses_for_plan : int ,
2023-11-14 11:59:48 +01:00
) - > None :
if billing_modality not in VALID_BILLING_MODALITY_VALUES : # nocoverage
raise BillingError ( " unknown billing_modality " , " " )
if schedule not in VALID_BILLING_SCHEDULE_VALUES : # nocoverage
raise BillingError ( " unknown schedule " )
if license_management not in VALID_LICENSE_MANAGEMENT_VALUES : # nocoverage
raise BillingError ( " unknown license_management " )
validate_licenses (
billing_modality == " charge_automatically " ,
licenses ,
seat_count ,
exempt_from_license_number_check ,
2023-12-09 08:16:53 +01:00
min_licenses_for_plan ,
2023-11-14 11:59:48 +01:00
)
2018-12-15 09:33:25 +01:00
# Be extremely careful changing this function. Historical billing periods
# are not stored anywhere, and are just computed on the fly using this
# function. Any change you make here should return the same value (or be
# within a few seconds) for basically any value from when the billing system
# went online to within a year from now.
def add_months ( dt : datetime , months : int ) - > datetime :
2021-02-12 08:19:30 +01:00
assert months > = 0
2018-12-15 09:33:25 +01:00
# It's fine that the max day in Feb is 28 for leap years.
2021-02-12 08:19:30 +01:00
MAX_DAY_FOR_MONTH = {
1 : 31 ,
2 : 28 ,
3 : 31 ,
4 : 30 ,
5 : 31 ,
6 : 30 ,
7 : 31 ,
8 : 31 ,
9 : 30 ,
10 : 31 ,
11 : 30 ,
12 : 31 ,
}
2018-12-15 09:33:25 +01:00
year = dt . year
month = dt . month + months
while month > 12 :
year + = 1
month - = 12
day = min ( dt . day , MAX_DAY_FOR_MONTH [ month ] )
# datetimes don't support leap seconds, so don't need to worry about those
return dt . replace ( year = year , month = month , day = day )
2021-02-12 08:19:30 +01:00
2018-12-15 09:33:25 +01:00
def next_month ( billing_cycle_anchor : datetime , dt : datetime ) - > datetime :
2021-02-12 08:19:30 +01:00
estimated_months = round ( ( dt - billing_cycle_anchor ) . days * 12.0 / 365 )
2018-12-15 09:33:25 +01:00
for months in range ( max ( estimated_months - 1 , 0 ) , estimated_months + 2 ) :
proposed_next_month = add_months ( billing_cycle_anchor , months )
if 20 < ( proposed_next_month - dt ) . days < 40 :
return proposed_next_month
2021-02-12 08:19:30 +01:00
raise AssertionError (
2021-02-12 08:20:45 +01:00
" Something wrong in next_month calculation with "
f " billing_cycle_anchor: { billing_cycle_anchor } , dt: { dt } "
2021-02-12 08:19:30 +01:00
)
2018-12-15 09:33:25 +01:00
2019-04-10 09:14:20 +02:00
def start_of_next_billing_cycle ( plan : CustomerPlan , event_time : datetime ) - > datetime :
2018-12-15 09:33:25 +01:00
months_per_period = {
2023-11-30 07:55:53 +01:00
CustomerPlan . BILLING_SCHEDULE_ANNUAL : 12 ,
CustomerPlan . BILLING_SCHEDULE_MONTHLY : 1 ,
2018-12-15 09:33:25 +01:00
} [ plan . billing_schedule ]
periods = 1
dt = plan . billing_cycle_anchor
2019-01-26 20:45:26 +01:00
while dt < = event_time :
2018-12-15 09:33:25 +01:00
dt = add_months ( plan . billing_cycle_anchor , months_per_period * periods )
periods + = 1
return dt
2021-02-12 08:19:30 +01:00
2024-07-12 02:30:23 +02:00
def next_invoice_date ( plan : CustomerPlan ) - > datetime | None :
2019-04-08 05:16:35 +02:00
if plan . status == CustomerPlan . ENDED :
return None
2021-02-12 08:19:30 +01:00
assert plan . next_invoice_date is not None # for mypy
2024-03-05 06:06:57 +01:00
months_per_period = 1
2019-01-28 22:57:29 +01:00
periods = 1
dt = plan . billing_cycle_anchor
while dt < = plan . next_invoice_date :
dt = add_months ( plan . billing_cycle_anchor , months_per_period * periods )
periods + = 1
return dt
2021-02-12 08:19:30 +01:00
2023-11-23 03:48:55 +01:00
def get_amount_to_credit_for_plan_tier_change (
current_plan : CustomerPlan , plan_change_date : datetime
) - > int :
last_renewal_ledger = (
LicenseLedger . objects . filter ( is_renewal = True , plan = current_plan ) . order_by ( " id " ) . last ( )
)
assert last_renewal_ledger is not None
assert current_plan . price_per_license is not None
next_renewal_date = start_of_next_billing_cycle ( current_plan , plan_change_date )
last_renewal_amount = last_renewal_ledger . licenses * current_plan . price_per_license
last_renewal_date = last_renewal_ledger . event_time
prorated_fraction = 1 - ( plan_change_date - last_renewal_date ) / (
next_renewal_date - last_renewal_date
)
amount_to_credit_back = math . ceil ( last_renewal_amount * prorated_fraction )
return amount_to_credit_back
2024-07-12 02:30:23 +02:00
def get_idempotency_key ( ledger_entry : LicenseLedger ) - > str | None :
2020-06-15 20:09:24 +02:00
if settings . TEST_SUITE :
return None
2021-02-12 08:20:45 +01:00
return f " ledger_entry: { ledger_entry . id } " # nocoverage
2020-06-15 20:09:24 +02:00
2021-02-12 08:19:30 +01:00
2020-10-20 15:46:04 +02:00
def cents_to_dollar_string ( cents : int ) - > str :
return f " { cents / 100. : ,.2f } "
2023-11-16 16:14:43 +01:00
# Should only be called if the customer is being charged automatically
def payment_method_string ( stripe_customer : stripe . Customer ) - > str :
assert stripe_customer . invoice_settings is not None
default_payment_method = stripe_customer . invoice_settings . default_payment_method
if default_payment_method is None :
return _ ( " No payment method on file. " )
assert isinstance ( default_payment_method , stripe . PaymentMethod )
if default_payment_method . type == " card " :
assert default_payment_method . card is not None
brand_name = default_payment_method . card . brand
if brand_name in CARD_CAPITALIZATION :
brand_name = CARD_CAPITALIZATION [ default_payment_method . card . brand ]
return _ ( " {brand} ending in {last4} " ) . format (
brand = brand_name ,
last4 = default_payment_method . card . last4 ,
)
# 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.
return _ ( " Unknown payment method. Please contact {email} . " ) . format (
email = settings . ZULIP_ADMINISTRATOR ,
) # nocoverage
2023-12-06 15:00:23 +01:00
def build_support_url ( support_view : str , query_text : str ) - > str :
2024-05-06 15:27:22 +02:00
support_realm_url = get_realm ( settings . STAFF_SUBDOMAIN ) . url
2023-12-06 15:00:23 +01:00
support_url = urljoin ( support_realm_url , reverse ( support_view ) )
query = urlencode ( { " q " : query_text } )
support_url = append_url_query_string ( support_url , query )
return support_url
2024-01-17 13:55:25 +01:00
def get_configured_fixed_price_plan_offer (
customer : Customer , plan_tier : int
2024-07-12 02:30:23 +02:00
) - > CustomerPlanOffer | None :
2024-01-17 13:55:25 +01:00
"""
Fixed price plan offer configured via / support which the
customer is yet to buy or schedule a purchase .
"""
if plan_tier == customer . required_plan_tier :
return CustomerPlanOffer . objects . filter (
customer = customer ,
tier = plan_tier ,
fixed_price__isnull = False ,
status = CustomerPlanOffer . CONFIGURED ,
) . first ( )
return None
2021-07-04 08:19:18 +02:00
class BillingError ( JsonableError ) :
data_fields = [ " error_description " ]
2018-08-06 06:16:29 +02:00
# error messages
2021-04-16 00:57:30 +02:00
CONTACT_SUPPORT = gettext_lazy ( " Something went wrong. Please contact {email} . " )
TRY_RELOADING = gettext_lazy ( " Something went wrong. Please reload the page. " )
2018-08-06 06:16:29 +02:00
2024-07-12 02:30:23 +02:00
def __init__ ( self , description : str , message : str | None = None ) - > None :
2021-07-04 08:19:18 +02:00
self . error_description = description
2020-10-17 03:42:50 +02:00
if message is None :
message = BillingError . CONTACT_SUPPORT . format ( email = settings . ZULIP_ADMINISTRATOR )
2021-07-04 08:19:18 +02:00
super ( ) . __init__ ( message )
2018-07-27 17:47:03 +02:00
2021-02-12 08:19:30 +01:00
2021-05-28 15:57:08 +02:00
class LicenseLimitError ( Exception ) :
pass
2018-08-06 23:07:26 +02:00
class StripeCardError ( BillingError ) :
pass
2021-02-12 08:19:30 +01:00
2018-08-06 23:07:26 +02:00
class StripeConnectionError ( BillingError ) :
pass
2021-02-12 08:19:30 +01:00
2023-12-13 02:44:55 +01:00
class ServerDeactivateWithExistingPlanError ( BillingError ) : # nocoverage
def __init__ ( self ) - > None :
super ( ) . __init__ (
" server deactivation with existing plan " ,
" " ,
)
2021-08-29 15:33:29 +02:00
class UpgradeWithExistingPlanError ( BillingError ) :
def __init__ ( self ) - > None :
super ( ) . __init__ (
" subscribing with existing subscription " ,
" The organization is already subscribed to a plan. Please reload the billing page. " ,
)
2023-12-05 07:39:21 +01:00
class InvalidPlanUpgradeError ( BillingError ) : # nocoverage
def __init__ ( self , message : str ) - > None :
super ( ) . __init__ (
" invalid plan upgrade " ,
message ,
)
2022-11-17 09:30:48 +01:00
class InvalidBillingScheduleError ( Exception ) :
2020-12-04 12:56:58 +01:00
def __init__ ( self , billing_schedule : int ) - > None :
self . message = f " Unknown billing_schedule: { billing_schedule } "
super ( ) . __init__ ( self . message )
2021-02-12 08:19:30 +01:00
2022-11-17 09:30:48 +01:00
class InvalidTierError ( Exception ) :
2021-09-15 13:54:56 +02:00
def __init__ ( self , tier : int ) - > None :
self . message = f " Unknown tier: { tier } "
super ( ) . __init__ ( self . message )
2024-01-03 20:22:49 +01:00
class SupportRequestError ( BillingError ) :
def __init__ ( self , message : str ) - > None :
super ( ) . __init__ (
" invalid support request " ,
message ,
)
2022-04-13 16:42:42 +02:00
def catch_stripe_errors ( func : Callable [ ParamT , ReturnT ] ) - > Callable [ ParamT , ReturnT ] :
2018-01-30 21:03:59 +01:00
@wraps ( func )
2022-04-13 16:42:42 +02:00
def wrapped ( * args : ParamT . args , * * kwargs : ParamT . kwargs ) - > ReturnT :
2018-01-30 21:03:59 +01:00
try :
return func ( * args , * * kwargs )
2018-08-06 23:07:26 +02:00
# See https://stripe.com/docs/api/python#error_handling, though
# https://stripe.com/docs/api/ruby#error_handling suggests there are additional fields, and
# https://stripe.com/docs/error-codes gives a more detailed set of error codes
2024-01-29 00:32:21 +01:00
except stripe . StripeError as e :
2023-11-14 21:48:14 +01:00
assert isinstance ( e . json_body , dict )
2021-02-12 08:20:45 +01:00
err = e . json_body . get ( " error " , { } )
2024-01-29 00:32:21 +01:00
if isinstance ( e , stripe . CardError ) :
2020-09-23 22:56:56 +02:00
billing_logger . info (
" Stripe card error: %s %s %s %s " ,
2021-02-12 08:19:30 +01:00
e . http_status ,
2021-02-12 08:20:45 +01:00
err . get ( " type " ) ,
err . get ( " code " ) ,
err . get ( " param " ) ,
2020-09-23 22:56:56 +02:00
)
# TODO: Look into i18n for this
2021-02-12 08:20:45 +01:00
raise StripeCardError ( " card error " , err . get ( " message " ) )
2020-05-02 20:57:12 +02:00
billing_logger . error (
" Stripe error: %s %s %s %s " ,
2021-02-12 08:19:30 +01:00
e . http_status ,
2021-02-12 08:20:45 +01:00
err . get ( " type " ) ,
err . get ( " code " ) ,
err . get ( " param " ) ,
2020-05-02 20:57:12 +02:00
)
2024-07-12 02:30:27 +02:00
if isinstance ( e , stripe . RateLimitError | stripe . APIConnectionError ) : # nocoverage TODO
2018-08-06 23:07:26 +02:00
raise StripeConnectionError (
2021-02-12 08:20:45 +01:00
" stripe connection error " ,
2021-02-12 08:19:30 +01:00
_ ( " Something went wrong. Please wait a few seconds and try again. " ) ,
)
2021-02-12 08:20:45 +01:00
raise BillingError ( " other stripe error " )
2021-02-12 08:19:30 +01:00
2022-04-13 16:42:42 +02:00
return wrapped
2018-01-30 21:03:59 +01:00
2021-02-12 08:19:30 +01:00
2018-01-30 21:03:59 +01:00
@catch_stripe_errors
2018-08-06 18:22:55 +02:00
def stripe_get_customer ( stripe_customer_id : str ) - > stripe . Customer :
2021-08-29 15:33:29 +02:00
return stripe . Customer . retrieve (
stripe_customer_id , expand = [ " invoice_settings " , " invoice_settings.default_payment_method " ]
)
2018-03-31 04:13:44 +02:00
2021-02-12 08:19:30 +01:00
2023-12-01 03:50:13 +01:00
def sponsorship_org_type_key_helper ( d : Any ) - > int :
return d [ 1 ] [ " display_order " ]
2023-11-30 14:49:10 +01:00
class PriceArgs ( TypedDict , total = False ) :
amount : int
unit_amount : int
quantity : int
2023-10-31 15:51:51 +01:00
@dataclass
class StripeCustomerData :
description : str
email : str
2024-07-12 02:30:17 +02:00
metadata : dict [ str , Any ]
2023-10-31 15:51:51 +01:00
2023-11-14 11:59:48 +01:00
@dataclass
class UpgradeRequest :
billing_modality : str
schedule : str
signed_seat_count : str
salt : str
2024-07-12 02:30:23 +02:00
license_management : str | None
licenses : int | None
2023-12-02 04:21:50 +01:00
tier : int
2024-07-12 02:30:23 +02:00
remote_server_plan_start_date : str | None
2023-11-14 11:59:48 +01:00
2023-11-20 08:40:09 +01:00
@dataclass
class InitialUpgradeRequest :
manual_license_management : bool
2023-11-22 07:36:24 +01:00
tier : int
2024-03-04 00:44:59 +01:00
billing_modality : str
2023-12-06 14:17:13 +01:00
success_message : str = " "
2023-11-20 08:40:09 +01:00
2023-11-22 12:44:02 +01:00
@dataclass
class UpdatePlanRequest :
2024-07-12 02:30:23 +02:00
status : int | None
licenses : int | None
licenses_at_next_renewal : int | None
schedule : int | None
2023-11-22 12:44:02 +01:00
2023-11-27 11:07:03 +01:00
@dataclass
class EventStatusRequest :
2024-07-12 02:30:23 +02:00
stripe_session_id : str | None
stripe_invoice_id : str | None
2023-11-27 11:07:03 +01:00
2023-11-30 21:11:54 +01:00
class SupportType ( Enum ) :
approve_sponsorship = 1
update_sponsorship_status = 2
attach_discount = 3
2023-12-01 12:23:31 +01:00
update_billing_modality = 4
2023-12-01 19:45:11 +01:00
modify_plan = 5
2023-12-14 19:55:38 +01:00
update_minimum_licenses = 6
2024-01-08 20:34:16 +01:00
update_plan_end_date = 7
2024-01-10 17:20:08 +01:00
update_required_plan_tier = 8
2024-01-17 13:55:25 +01:00
configure_fixed_price_plan = 9
2024-02-13 13:23:07 +01:00
delete_fixed_price_next_plan = 10
2024-08-15 18:29:51 +02:00
configure_temporary_courtesy_plan = 11
2023-11-30 21:11:54 +01:00
class SupportViewRequest ( TypedDict , total = False ) :
support_type : SupportType
2024-07-12 02:30:23 +02:00
sponsorship_status : bool | None
monthly_discounted_price : int | None
annual_discounted_price : int | None
billing_modality : str | None
plan_modification : str | None
new_plan_tier : int | None
minimum_licenses : int | None
plan_end_date : str | None
required_plan_tier : int | None
fixed_price : int | None
sent_invoice_id : str | None
2023-11-30 21:11:54 +01:00
2024-08-29 20:48:40 +02:00
class BillingSessionEventType ( IntEnum ) :
2023-11-02 17:44:02 +01:00
STRIPE_CUSTOMER_CREATED = 1
STRIPE_CARD_CHANGED = 2
CUSTOMER_PLAN_CREATED = 3
DISCOUNT_CHANGED = 4
2023-11-02 15:23:35 +01:00
SPONSORSHIP_APPROVED = 5
2023-11-02 18:17:08 +01:00
SPONSORSHIP_PENDING_STATUS_CHANGED = 6
2023-12-01 13:19:04 +01:00
BILLING_MODALITY_CHANGED = 7
2023-11-13 15:05:56 +01:00
CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN = 8
2023-11-20 13:01:25 +01:00
CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN = 9
2023-12-04 23:20:49 +01:00
BILLING_ENTITY_PLAN_TYPE_CHANGED = 10
2024-01-12 17:38:55 +01:00
CUSTOMER_PROPERTY_CHANGED = 11
CUSTOMER_PLAN_PROPERTY_CHANGED = 12
2023-11-02 17:44:02 +01:00
2023-11-30 17:11:41 +01:00
class PlanTierChangeType ( Enum ) :
INVALID = 1
UPGRADE = 2
DOWNGRADE = 3
2023-11-02 17:44:02 +01:00
class BillingSessionAuditLogEventError ( Exception ) :
2024-08-29 20:48:40 +02:00
def __init__ ( self , event_type : BillingSessionEventType ) - > None :
2023-11-02 17:44:02 +01:00
self . message = f " Unknown audit log event type: { event_type } "
super ( ) . __init__ ( self . message )
2024-02-16 22:56:36 +01:00
# Sync this with upgrade_params_schema in base_page_params.ts.
2023-11-24 07:29:06 +01:00
class UpgradePageParams ( TypedDict ) :
2024-02-16 22:56:36 +01:00
page_type : Literal [ " upgrade " ]
2023-11-24 07:29:06 +01:00
annual_price : int
2024-07-12 02:30:23 +02:00
demo_organization_scheduled_deletion_date : datetime | None
2023-11-24 07:29:06 +01:00
monthly_price : int
seat_count : int
2023-12-01 04:18:58 +01:00
billing_base_url : str
2023-12-18 13:02:36 +01:00
tier : int
2023-12-20 07:24:21 +01:00
flat_discount : int
flat_discounted_months : int
2024-07-12 02:30:23 +02:00
fixed_price : int | None
2024-03-04 00:44:59 +01:00
setup_payment_by_invoice : bool
2024-07-12 02:30:23 +02:00
free_trial_days : int | None
percent_off_annual_price : str | None
percent_off_monthly_price : str | None
2023-11-24 07:29:06 +01:00
class UpgradePageSessionTypeSpecificContext ( TypedDict ) :
customer_name : str
email : str
is_demo_organization : bool
2024-07-12 02:30:23 +02:00
demo_organization_scheduled_deletion_date : datetime | None
2023-11-24 07:29:06 +01:00
is_self_hosting : bool
2023-11-30 01:48:46 +01:00
class SponsorshipApplicantInfo ( TypedDict ) :
name : str
role : str
email : str
class SponsorshipRequestSessionSpecificContext ( TypedDict ) :
# We don't store UserProfile for remote realms.
2024-07-12 02:30:23 +02:00
realm_user : UserProfile | None
2023-11-30 01:48:46 +01:00
user_info : SponsorshipApplicantInfo
# TODO: Call this what we end up calling it for /support page.
realm_string_id : str
2023-11-24 07:29:06 +01:00
class UpgradePageContext ( TypedDict ) :
customer_name : str
email : str
exempt_from_license_number_check : bool
2024-07-12 02:30:23 +02:00
free_trial_end_date : str | None
2023-11-24 07:29:06 +01:00
is_demo_organization : bool
manual_license_management : bool
2023-12-09 08:16:53 +01:00
using_min_licenses_for_plan : bool
2023-12-18 12:16:29 +01:00
min_licenses_for_plan : int
2023-11-24 07:29:06 +01:00
page_params : UpgradePageParams
2024-07-12 02:30:23 +02:00
payment_method : str | None
2023-11-24 07:29:06 +01:00
plan : str
2024-01-17 13:55:25 +01:00
fixed_price_plan : bool
2024-07-12 02:30:23 +02:00
pay_by_invoice_payments_page : str | None
remote_server_legacy_plan_end_date : str | None
2023-11-24 07:29:06 +01:00
salt : str
seat_count : int
signed_seat_count : str
2023-12-06 14:17:13 +01:00
success_message : str
2023-12-12 07:02:08 +01:00
is_sponsorship_pending : bool
sponsorship_plan_name : str
2024-07-12 02:30:23 +02:00
scheduled_upgrade_invoice_amount_due : str | None
2024-04-10 10:31:15 +02:00
is_free_trial_invoice_expired_notice : bool
2024-07-12 02:30:23 +02:00
free_trial_invoice_expired_notice_page_plan_name : str | None
2023-11-24 07:29:06 +01:00
2023-11-30 01:48:46 +01:00
class SponsorshipRequestForm ( forms . Form ) :
2024-07-16 22:11:43 +02:00
website = forms . URLField (
max_length = ZulipSponsorshipRequest . MAX_ORG_URL_LENGTH , required = False , assume_scheme = " https "
)
2023-11-30 01:48:46 +01:00
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 )
2023-12-08 08:25:05 +01:00
requested_plan = forms . ChoiceField (
choices = [ ( plan . value , plan . name ) for plan in SponsoredPlanTypes ] , required = False
)
2023-11-30 01:48:46 +01:00
2023-10-26 14:11:43 +02:00
class BillingSession ( ABC ) :
2023-11-30 21:11:54 +01:00
@property
@abstractmethod
def billing_entity_display_name ( self ) - > str :
pass
2023-11-06 13:52:12 +01:00
@property
@abstractmethod
def billing_session_url ( self ) - > str :
pass
2023-12-01 04:18:58 +01:00
@property
@abstractmethod
def billing_base_url ( self ) - > str :
pass
2023-11-30 01:48:46 +01:00
@abstractmethod
def support_url ( self ) - > str :
pass
2023-10-26 14:11:43 +02:00
@abstractmethod
2024-07-12 02:30:23 +02:00
def get_customer ( self ) - > Customer | None :
2023-10-26 14:11:43 +02:00
pass
2023-12-02 03:54:24 +01:00
@abstractmethod
def get_email ( self ) - > str :
pass
2023-11-08 17:02:31 +01:00
@abstractmethod
2023-12-08 13:19:24 +01:00
def current_count_for_billed_licenses ( self , event_time : datetime = timezone_now ( ) ) - > int :
2023-11-08 17:02:31 +01:00
pass
2023-11-02 17:44:02 +01:00
@abstractmethod
2024-08-29 20:48:40 +02:00
def get_audit_log_event ( self , event_type : BillingSessionEventType ) - > int :
2023-11-02 17:44:02 +01:00
pass
2023-10-26 14:11:43 +02:00
@abstractmethod
def write_to_audit_log (
2023-11-02 17:44:02 +01:00
self ,
2024-08-29 20:48:40 +02:00
event_type : BillingSessionEventType ,
2023-11-02 17:44:02 +01:00
event_time : datetime ,
* ,
2023-12-24 15:56:33 +01:00
background_update : bool = False ,
2024-07-12 02:30:23 +02:00
extra_data : dict [ str , Any ] | None = None ,
2023-10-26 14:11:43 +02:00
) - > None :
pass
@abstractmethod
2023-10-31 15:51:51 +01:00
def get_data_for_stripe_customer ( self ) - > StripeCustomerData :
2023-10-26 14:11:43 +02:00
pass
2023-11-06 15:51:54 +01:00
@abstractmethod
2024-02-10 07:47:32 +01:00
def update_data_for_checkout_session_and_invoice_payment (
2024-07-12 02:30:17 +02:00
self , metadata : dict [ str , Any ]
) - > dict [ str , Any ] :
2023-11-06 15:51:54 +01:00
pass
2024-02-01 05:07:01 +01:00
@abstractmethod
def org_name ( self ) - > str :
pass
2024-04-08 11:58:02 +02:00
def customer_plan_exists ( self ) - > bool :
# Checks if the realm / server had a plan anytime in the past.
customer = self . get_customer ( )
if customer is not None and CustomerPlan . objects . filter ( customer = customer ) . exists ( ) :
return True
2024-03-15 03:03:11 +01:00
if isinstance ( self , RemoteRealmBillingSession ) :
2024-04-08 11:58:02 +02:00
return CustomerPlan . objects . filter (
customer = get_customer_by_remote_server ( self . remote_realm . server )
) . exists ( )
2024-03-15 03:03:11 +01:00
2024-04-08 11:58:02 +02:00
return False
2024-03-15 03:03:11 +01:00
2024-02-01 16:24:05 +01:00
def get_past_invoices_session_url ( self ) - > str :
headline = " List of past invoices "
customer = self . get_customer ( )
assert customer is not None and customer . stripe_customer_id is not None
# Check if customer has any $0 invoices.
2024-04-30 19:35:43 +02:00
list_params = stripe . Invoice . ListParams (
2024-02-01 16:24:05 +01:00
customer = customer . stripe_customer_id ,
limit = 1 ,
status = " paid " ,
2024-04-30 19:35:43 +02:00
)
list_params [ " total " ] = 0 # type: ignore[typeddict-unknown-key] # Not documented or annotated, but https://github.com/zulip/zulip/pull/28785/files#r1477005528 says it works
if stripe . Invoice . list ( * * list_params ) . data : # nocoverage
2024-02-01 16:24:05 +01:00
# These are payment for upgrades which were paid directly by the customer and then we
# created an invoice for them resulting in `$0` invoices since there was no amount due.
headline + = " ($0 invoices include payment) "
configuration = stripe . billing_portal . Configuration . create (
business_profile = {
" headline " : headline ,
} ,
features = {
" invoice_history " : { " enabled " : True } ,
} ,
)
return stripe . billing_portal . Session . create (
customer = customer . stripe_customer_id ,
configuration = configuration . id ,
return_url = f " { self . billing_session_url } /billing/ " ,
) . url
2024-02-08 13:37:55 +01:00
def get_stripe_customer_portal_url (
self ,
return_to_billing_page : bool ,
manual_license_management : bool ,
2024-07-12 02:30:23 +02:00
tier : int | None = None ,
2024-03-04 00:44:59 +01:00
setup_payment_by_invoice : bool = False ,
2024-02-08 13:37:55 +01:00
) - > str :
customer = self . get_customer ( )
2024-03-04 00:44:59 +01:00
if setup_payment_by_invoice and (
customer is None or customer . stripe_customer_id is None
) : # nocoverage
customer = self . create_stripe_customer ( )
2024-02-08 13:37:55 +01:00
assert customer is not None and customer . stripe_customer_id is not None
if return_to_billing_page :
return_url = f " { self . billing_session_url } /billing/ "
else :
assert tier is not None
base_return_url = f " { self . billing_session_url } /upgrade/ "
params = {
" manual_license_management " : str ( manual_license_management ) . lower ( ) ,
" tier " : str ( tier ) ,
2024-03-04 00:44:59 +01:00
" setup_payment_by_invoice " : str ( setup_payment_by_invoice ) . lower ( ) ,
2024-02-08 13:37:55 +01:00
}
return_url = f " { base_return_url } ? { urlencode ( params ) } "
configuration = stripe . billing_portal . Configuration . create (
business_profile = {
" headline " : " Invoice and receipt billing information " ,
} ,
features = { " customer_update " : { " enabled " : True , " allowed_updates " : [ " address " , " name " ] } } ,
)
return stripe . billing_portal . Session . create (
customer = customer . stripe_customer_id ,
configuration = configuration . id ,
return_url = return_url ,
) . url
2024-02-10 07:47:32 +01:00
def generate_invoice_for_upgrade (
2023-12-02 04:21:50 +01:00
self ,
2023-12-20 07:24:21 +01:00
customer : Customer ,
2024-07-12 02:30:23 +02:00
price_per_license : int | None ,
fixed_price : int | None ,
2023-12-02 04:21:50 +01:00
licenses : int ,
plan_tier : int ,
2023-12-20 07:24:21 +01:00
billing_schedule : int ,
2024-02-10 07:47:32 +01:00
charge_automatically : bool ,
2024-04-30 19:23:16 +02:00
invoice_period : stripe . InvoiceItem . CreateParamsPeriod ,
2024-07-12 02:30:23 +02:00
license_management : str | None = None ,
days_until_due : int | None = None ,
2024-04-10 10:31:15 +02:00
on_free_trial : bool = False ,
2024-07-12 02:30:23 +02:00
current_plan_id : int | None = None ,
2024-02-10 07:47:32 +01:00
) - > stripe . Invoice :
2024-04-30 19:43:16 +02:00
assert customer . stripe_customer_id is not None
2023-12-02 04:21:50 +01:00
plan_name = CustomerPlan . name_from_tier ( plan_tier )
2024-01-17 13:55:25 +01:00
assert price_per_license is None or fixed_price is None
2024-02-10 07:47:32 +01:00
price_args : PriceArgs = { }
if fixed_price is None :
assert price_per_license is not None
price_args = {
" quantity " : licenses ,
" unit_amount " : price_per_license ,
}
2024-01-17 13:55:25 +01:00
else :
assert fixed_price is not None
2024-02-10 07:47:32 +01:00
amount_due = get_amount_due_fixed_price_plan ( fixed_price , billing_schedule )
price_args = { " amount " : amount_due }
stripe . InvoiceItem . create (
currency = " usd " ,
customer = customer . stripe_customer_id ,
description = plan_name ,
discountable = False ,
2024-03-30 04:59:59 +01:00
period = invoice_period ,
2024-02-10 07:47:32 +01:00
* * price_args ,
)
2024-01-17 13:55:25 +01:00
if fixed_price is None and customer . flat_discounted_months > 0 :
2023-12-20 07:24:21 +01:00
num_months = 12 if billing_schedule == CustomerPlan . BILLING_SCHEDULE_ANNUAL else 1
flat_discounted_months = min ( customer . flat_discounted_months , num_months )
2024-02-10 07:47:32 +01:00
discount = customer . flat_discount * flat_discounted_months
customer . flat_discounted_months - = flat_discounted_months
customer . save ( update_fields = [ " flat_discounted_months " ] )
stripe . InvoiceItem . create (
currency = " usd " ,
customer = customer . stripe_customer_id ,
description = f " $ { cents_to_dollar_string ( customer . flat_discount ) } /month new customer discount " ,
# Negative value to apply discount.
amount = ( - 1 * discount ) ,
2024-03-30 04:59:59 +01:00
period = invoice_period ,
2024-02-10 07:47:32 +01:00
)
if charge_automatically :
2024-06-10 20:35:19 +02:00
collection_method : Literal [ " charge_automatically " , " send_invoice " ] = (
2024-04-30 19:23:16 +02:00
" charge_automatically "
)
2024-02-10 07:47:32 +01:00
else :
collection_method = " send_invoice "
2024-03-04 00:44:59 +01:00
# days_until_due is required for `send_invoice` collection method. Since this is an invoice
# for upgrade, the due date is irrelevant since customer will upgrade once they pay the invoice
# regardless of the due date. Using `1` shows `Due today / tomorrow` which seems nice.
2024-04-10 10:31:15 +02:00
if days_until_due is None :
days_until_due = 1
2024-02-10 07:47:32 +01:00
metadata = {
2024-04-30 19:20:47 +02:00
" plan_tier " : str ( plan_tier ) ,
" billing_schedule " : str ( billing_schedule ) ,
" licenses " : str ( licenses ) ,
" license_management " : str ( license_management ) ,
" on_free_trial " : str ( on_free_trial ) ,
" current_plan_id " : str ( current_plan_id ) ,
2024-02-10 07:47:32 +01:00
}
if hasattr ( self , " user " ) :
metadata [ " user_id " ] = self . user . id
# We only need to email customer about open invoice for manual billing.
# If automatic charge fails, we simply void the invoice.
# https://stripe.com/docs/invoicing/integration/automatic-advancement-collection
auto_advance = not charge_automatically
2024-04-30 19:37:55 +02:00
invoice_params = stripe . Invoice . CreateParams (
2024-02-10 07:47:32 +01:00
auto_advance = auto_advance ,
collection_method = collection_method ,
customer = customer . stripe_customer_id ,
statement_descriptor = plan_name ,
metadata = metadata ,
2023-12-02 04:21:50 +01:00
)
2024-04-30 19:37:55 +02:00
if days_until_due is not None :
invoice_params [ " days_until_due " ] = days_until_due
stripe_invoice = stripe . Invoice . create ( * * invoice_params )
2024-02-10 07:47:32 +01:00
stripe . Invoice . finalize_invoice ( stripe_invoice )
return stripe_invoice
2023-11-06 15:51:54 +01:00
2023-10-26 14:11:43 +02:00
@abstractmethod
2023-10-31 19:22:55 +01:00
def update_or_create_customer (
2024-07-12 02:30:23 +02:00
self , stripe_customer_id : str | None = None , * , defaults : dict [ str , Any ] | None = None
2023-10-31 19:22:55 +01:00
) - > Customer :
2023-10-26 14:11:43 +02:00
pass
2023-11-08 17:15:40 +01:00
@abstractmethod
2023-12-24 15:56:33 +01:00
def do_change_plan_type (
2024-07-12 02:30:23 +02:00
self , * , tier : int | None , is_sponsored : bool = False , background_update : bool = False
2023-12-24 15:56:33 +01:00
) - > None :
2023-11-08 17:15:40 +01:00
pass
2023-11-13 15:05:56 +01:00
@abstractmethod
2023-12-24 15:56:33 +01:00
def process_downgrade ( self , plan : CustomerPlan , background_update : bool = False ) - > None :
2023-11-13 15:05:56 +01:00
pass
2023-11-02 15:23:35 +01:00
@abstractmethod
2023-11-30 21:11:54 +01:00
def approve_sponsorship ( self ) - > str :
2023-11-02 15:23:35 +01:00
pass
2023-11-27 13:25:11 +01:00
@abstractmethod
def is_sponsored ( self ) - > bool :
pass
2023-11-30 01:48:46 +01:00
@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
2023-11-20 08:40:09 +01:00
@abstractmethod
2023-11-24 07:29:06 +01:00
def get_upgrade_page_session_type_specific_context (
self ,
) - > UpgradePageSessionTypeSpecificContext :
2023-11-20 08:40:09 +01:00
pass
2024-01-10 17:20:08 +01:00
@abstractmethod
def check_plan_tier_is_billable ( self , plan_tier : int ) - > bool :
pass
2023-11-23 07:29:03 +01:00
@abstractmethod
2023-11-30 17:11:41 +01:00
def get_type_of_plan_tier_change (
self , current_plan_tier : int , new_plan_tier : int
) - > PlanTierChangeType :
2023-11-23 07:29:03 +01:00
pass
2023-11-27 11:07:03 +01:00
@abstractmethod
def has_billing_access ( self ) - > bool :
pass
2023-11-27 13:08:43 +01:00
@abstractmethod
def on_paid_plan ( self ) - > bool :
pass
@abstractmethod
2024-07-12 02:30:17 +02:00
def add_sponsorship_info_to_context ( self , context : dict [ str , Any ] ) - > None :
2023-11-27 13:08:43 +01:00
pass
2023-12-01 03:51:05 +01:00
@abstractmethod
2024-07-12 02:30:17 +02:00
def get_metadata_for_stripe_update_card ( self ) - > dict [ str , str ] :
2023-12-01 03:51:05 +01:00
pass
2023-12-12 09:02:17 +01:00
@abstractmethod
def sync_license_ledger_if_needed ( self ) - > None :
# Updates the license ledger based on RemoteRealmAuditLog
# entries.
#
# Supports backfilling entries from weeks if the past if
# needed when we receive audit logs, making any end-of-cycle
# updates that happen to be scheduled inside the interval that
# we are processing.
#
# But this support is fragile, in that it does not handle the
# possibility that some other code path changed or ended the
# customer's current plan at some point after
# last_ledger.event_time but before the event times for the
# audit logs we will be processing.
pass
2024-07-12 02:30:23 +02:00
def is_sponsored_or_pending ( self , customer : Customer | None ) - > bool :
2023-12-04 14:33:06 +01:00
if ( customer is not None and customer . sponsorship_pending ) or self . is_sponsored ( ) :
return True
return False
2023-12-04 14:11:35 +01:00
def get_remote_server_legacy_plan (
2024-07-12 02:30:23 +02:00
self , customer : Customer | None , status : int = CustomerPlan . ACTIVE
) - > CustomerPlan | None :
2023-12-04 14:11:35 +01:00
# status = CustomerPlan.ACTIVE means that the legacy plan is not scheduled for an upgrade.
# status = CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END means that the legacy plan is scheduled for an upgrade.
if customer is None :
return None
return CustomerPlan . objects . filter (
customer = customer ,
tier = CustomerPlan . TIER_SELF_HOSTED_LEGACY ,
status = status ,
) . first ( )
def get_formatted_remote_server_legacy_plan_end_date (
2024-07-12 02:30:23 +02:00
self , customer : Customer | None , status : int = CustomerPlan . ACTIVE
) - > str | None : # nocoverage
2023-12-04 14:11:35 +01:00
plan = self . get_remote_server_legacy_plan ( customer , status )
if plan is None :
return None
assert plan . end_date is not None
return plan . end_date . strftime ( " % B %d , % Y " )
2024-07-12 02:30:23 +02:00
def get_legacy_remote_server_next_plan ( self , customer : Customer ) - > CustomerPlan | None :
2023-12-04 14:21:07 +01:00
legacy_plan = self . get_remote_server_legacy_plan (
customer , CustomerPlan . SWITCH_PLAN_TIER_AT_PLAN_END
)
if legacy_plan is None :
return None
# This also asserts that such a plan should exist.
assert legacy_plan . end_date is not None
return CustomerPlan . objects . get (
customer = customer ,
billing_cycle_anchor = legacy_plan . end_date ,
status = CustomerPlan . NEVER_STARTED ,
2023-12-09 09:12:46 +01:00
)
2024-07-12 02:30:23 +02:00
def get_legacy_remote_server_next_plan_name ( self , customer : Customer ) - > str | None :
2023-12-09 09:12:46 +01:00
next_plan = self . get_legacy_remote_server_next_plan ( customer )
if next_plan is None :
return None
return next_plan . name
2023-12-04 14:21:07 +01:00
2023-10-26 14:11:43 +02:00
@catch_stripe_errors
2023-11-09 14:46:39 +01:00
def create_stripe_customer ( self ) - > Customer :
2023-10-26 14:11:43 +02:00
stripe_customer_data = self . get_data_for_stripe_customer ( )
stripe_customer = stripe . Customer . create (
2023-10-31 15:51:51 +01:00
description = stripe_customer_data . description ,
email = stripe_customer_data . email ,
metadata = stripe_customer_data . metadata ,
2021-02-12 08:19:30 +01:00
)
2023-10-26 14:11:43 +02:00
event_time = timestamp_to_datetime ( stripe_customer . created )
with transaction . atomic ( ) :
2024-08-29 20:48:40 +02:00
self . write_to_audit_log ( BillingSessionEventType . STRIPE_CUSTOMER_CREATED , event_time )
2023-10-26 14:11:43 +02:00
customer = self . update_or_create_customer ( stripe_customer . id )
return customer
@catch_stripe_errors
def replace_payment_method (
self , stripe_customer_id : str , payment_method : str , pay_invoices : bool = False
) - > None :
stripe . Customer . modify (
stripe_customer_id , invoice_settings = { " default_payment_method " : payment_method }
)
2024-08-29 20:48:40 +02:00
self . write_to_audit_log ( BillingSessionEventType . STRIPE_CARD_CHANGED , timezone_now ( ) )
2023-10-26 14:11:43 +02:00
if pay_invoices :
for stripe_invoice in stripe . Invoice . list (
collection_method = " charge_automatically " ,
customer = stripe_customer_id ,
status = " open " ,
) :
# The stripe customer with the associated ID will get either a receipt
# or a "failed payment" email, but the in-app messaging could be clearer
# here (e.g. it could explicitly tell the user that there were payment(s)
# and that they succeeded or failed). Worth fixing if we notice that a
# lot of cards end up failing at this step.
stripe . Invoice . pay ( stripe_invoice )
# Returns Customer instead of stripe_customer so that we don't make a Stripe
# API call if there's nothing to update
@catch_stripe_errors
2024-07-12 02:30:23 +02:00
def update_or_create_stripe_customer ( self , payment_method : str | None = None ) - > Customer :
2023-10-26 14:11:43 +02:00
customer = self . get_customer ( )
if customer is None or customer . stripe_customer_id is None :
2024-02-10 07:47:32 +01:00
# A stripe.PaymentMethod should be attached to a stripe.Customer via replace_payment_method.
# Here we just want to create a new stripe.Customer.
2023-11-09 14:46:39 +01:00
assert payment_method is None
2023-10-26 14:11:43 +02:00
# We could do a better job of handling race conditions here, but if two
# people try to upgrade at exactly the same time, the main bad thing that
2023-11-09 14:46:39 +01:00
# will happen is that we will create an extra stripe.Customer that we can
2023-10-26 14:11:43 +02:00
# delete or ignore.
2023-11-09 14:46:39 +01:00
return self . create_stripe_customer ( )
2021-08-29 15:33:29 +02:00
if payment_method is not None :
2023-10-26 14:11:43 +02:00
self . replace_payment_method ( customer . stripe_customer_id , payment_method , True )
return customer
2024-02-10 07:47:32 +01:00
def create_stripe_invoice_and_charge (
2024-01-17 13:55:25 +01:00
self ,
2024-07-12 02:30:17 +02:00
metadata : dict [ str , Any ] ,
2023-11-18 11:29:04 +01:00
) - > str :
2024-03-15 08:41:50 +01:00
"""
Charge customer based on ` billing_modality ` . If ` billing_modality ` is ` charge_automatically ` ,
charge customer immediately . If the charge fails , the invoice will be voided .
If ` billing_modality ` is ` send_invoice ` , create an invoice and send it to the customer .
"""
2023-11-06 15:51:54 +01:00
customer = self . get_customer ( )
assert customer is not None and customer . stripe_customer_id is not None
2024-03-04 00:44:59 +01:00
charge_automatically = metadata [ " billing_modality " ] == " charge_automatically "
2023-11-18 11:29:04 +01:00
# Ensure customers have a default payment method set.
stripe_customer = stripe_get_customer ( customer . stripe_customer_id )
2024-03-04 00:44:59 +01:00
if charge_automatically and not stripe_customer_has_credit_card_as_default_payment_method (
stripe_customer
) :
2023-11-18 11:29:04 +01:00
raise BillingError (
" no payment method " ,
" Please add a credit card before upgrading. " ,
)
2024-03-04 00:44:59 +01:00
if charge_automatically :
assert stripe_customer . invoice_settings is not None
assert stripe_customer . invoice_settings . default_payment_method is not None
2024-02-10 07:47:32 +01:00
stripe_invoice = None
2023-11-18 11:29:04 +01:00
try :
2024-04-10 10:31:15 +02:00
current_plan_id = metadata . get ( " current_plan_id " )
on_free_trial = bool ( metadata . get ( " on_free_trial " ) )
2024-02-10 07:47:32 +01:00
stripe_invoice = self . generate_invoice_for_upgrade (
customer ,
metadata [ " price_per_license " ] ,
metadata [ " fixed_price " ] ,
metadata [ " licenses " ] ,
metadata [ " plan_tier " ] ,
metadata [ " billing_schedule " ] ,
2024-03-04 00:44:59 +01:00
charge_automatically = charge_automatically ,
2024-02-10 07:47:32 +01:00
license_management = metadata [ " license_management " ] ,
2024-03-30 04:59:59 +01:00
invoice_period = metadata [ " invoice_period " ] ,
2024-04-10 10:31:15 +02:00
days_until_due = metadata . get ( " days_until_due " ) ,
on_free_trial = on_free_trial ,
current_plan_id = current_plan_id ,
2024-02-10 07:47:32 +01:00
)
assert stripe_invoice . id is not None
2024-04-10 10:31:15 +02:00
2024-02-10 07:47:32 +01:00
invoice = Invoice . objects . create (
stripe_invoice_id = stripe_invoice . id ,
customer = customer ,
status = Invoice . SENT ,
2024-04-10 10:31:15 +02:00
plan_id = current_plan_id ,
is_created_for_free_trial_upgrade = current_plan_id is not None and on_free_trial ,
2023-11-18 11:29:04 +01:00
)
2024-04-10 10:31:15 +02:00
2024-03-04 00:44:59 +01:00
if charge_automatically :
# Stripe takes its sweet hour to charge customers after creating an invoice.
# Since we want to charge customers immediately, we charge them manually.
# Then poll for the status of the invoice to see if the payment succeeded.
stripe_invoice = stripe . Invoice . pay ( stripe_invoice . id )
2024-02-10 07:47:32 +01:00
except Exception as e :
if stripe_invoice is not None :
assert stripe_invoice . id is not None
# Void invoice to avoid double charging if customer tries to upgrade again.
stripe . Invoice . void_invoice ( stripe_invoice . id )
invoice . status = Invoice . VOID
invoice . save ( update_fields = [ " status " ] )
if isinstance ( e , stripe . CardError ) :
raise StripeCardError ( " card error " , e . user_message )
else : # nocoverage
raise e
2023-11-18 11:29:04 +01:00
2024-02-10 07:47:32 +01:00
assert stripe_invoice . id is not None
return stripe_invoice . id
2023-11-18 11:29:04 +01:00
2023-12-02 09:09:43 +01:00
def create_card_update_session_for_upgrade (
2023-11-18 11:29:04 +01:00
self ,
2023-11-21 12:41:38 +01:00
manual_license_management : bool ,
2023-12-18 13:02:36 +01:00
tier : int ,
2024-07-12 02:30:17 +02:00
) - > dict [ str , Any ] :
2023-12-02 09:05:20 +01:00
metadata = self . get_metadata_for_stripe_update_card ( )
2023-11-18 11:29:04 +01:00
customer = self . update_or_create_stripe_customer ( )
2024-04-30 19:43:16 +02:00
assert customer . stripe_customer_id is not None
2023-12-18 13:02:36 +01:00
# URL when user cancels the card update session.
base_cancel_url = f " { self . billing_session_url } /upgrade/ "
params = {
" manual_license_management " : str ( manual_license_management ) . lower ( ) ,
" tier " : str ( tier ) ,
}
cancel_url = f " { base_cancel_url } ? { urlencode ( params ) } "
2023-11-21 12:41:38 +01:00
2023-11-18 11:29:04 +01:00
stripe_session = stripe . checkout . Session . create (
2023-11-21 12:41:38 +01:00
cancel_url = cancel_url ,
2023-11-06 15:51:54 +01:00
customer = customer . stripe_customer_id ,
metadata = metadata ,
2023-11-18 11:29:04 +01:00
mode = " setup " ,
payment_method_types = [ " card " ] ,
2024-01-17 20:02:44 +01:00
success_url = f " { self . billing_session_url } /billing/event_status/?stripe_session_id= {{ CHECKOUT_SESSION_ID }} " ,
2024-02-02 20:11:25 +01:00
billing_address_collection = " required " ,
2024-02-08 12:00:58 +01:00
customer_update = { " address " : " auto " , " name " : " auto " } ,
2023-11-06 15:51:54 +01:00
)
2023-11-18 11:29:04 +01:00
Session . objects . create (
stripe_session_id = stripe_session . id ,
2023-11-06 15:51:54 +01:00
customer = customer ,
2023-12-01 03:51:05 +01:00
type = Session . CARD_UPDATE_FROM_UPGRADE_PAGE ,
2023-11-21 12:41:38 +01:00
is_manual_license_management_upgrade_session = manual_license_management ,
2023-12-18 13:02:36 +01:00
tier = tier ,
2023-11-06 15:51:54 +01:00
)
2023-12-01 03:51:05 +01:00
return {
" stripe_session_url " : stripe_session . url ,
" stripe_session_id " : stripe_session . id ,
}
2023-11-06 15:51:54 +01:00
2024-07-12 02:30:17 +02:00
def create_card_update_session ( self ) - > dict [ str , Any ] :
2023-12-02 09:09:43 +01:00
metadata = self . get_metadata_for_stripe_update_card ( )
2023-11-06 13:52:12 +01:00
customer = self . get_customer ( )
assert customer is not None and customer . stripe_customer_id is not None
stripe_session = stripe . checkout . Session . create (
cancel_url = f " { self . billing_session_url } /billing/ " ,
customer = customer . stripe_customer_id ,
metadata = metadata ,
mode = " setup " ,
payment_method_types = [ " card " ] ,
2024-01-17 20:02:44 +01:00
success_url = f " { self . billing_session_url } /billing/event_status/?stripe_session_id= {{ CHECKOUT_SESSION_ID }} " ,
2024-02-02 20:11:25 +01:00
billing_address_collection = " required " ,
2023-11-06 13:52:12 +01:00
)
2023-11-18 11:29:04 +01:00
Session . objects . create (
2023-11-06 13:52:12 +01:00
stripe_session_id = stripe_session . id ,
customer = customer ,
2023-12-02 09:09:43 +01:00
type = Session . CARD_UPDATE_FROM_BILLING_PAGE ,
2023-11-06 13:52:12 +01:00
)
2023-12-02 09:09:43 +01:00
return {
" stripe_session_url " : stripe_session . url ,
" stripe_session_id " : stripe_session . id ,
}
2023-11-06 13:52:12 +01:00
2023-12-09 09:21:53 +01:00
def apply_discount_to_plan (
self ,
plan : CustomerPlan ,
2024-05-06 06:12:15 +02:00
customer : Customer ,
2023-12-09 09:21:53 +01:00
) - > None :
2024-05-06 06:12:15 +02:00
original_plan_price = get_price_per_license ( plan . tier , plan . billing_schedule )
plan . price_per_license = get_price_per_license ( plan . tier , plan . billing_schedule , customer )
# For display purposes only.
plan . discount = format_discount_percentage (
Decimal ( ( original_plan_price - plan . price_per_license ) / original_plan_price * 100 )
)
plan . save ( update_fields = [ " price_per_license " , " discount " ] )
2023-12-09 09:21:53 +01:00
2024-05-06 06:12:15 +02:00
def attach_discount_to_customer (
self , monthly_discounted_price : int , annual_discounted_price : int
) - > str :
2023-12-20 07:24:21 +01:00
# Remove flat discount if giving customer a percentage discount.
2023-10-31 19:22:55 +01:00
customer = self . get_customer ( )
2024-04-29 11:16:54 +02:00
# We set required plan tier before setting a discount for the customer, so it's always defined.
assert customer is not None
assert customer . required_plan_tier is not None
2024-05-06 06:12:15 +02:00
old_monthly_discounted_price = customer . monthly_discounted_price
customer . monthly_discounted_price = monthly_discounted_price
old_annual_discounted_price = customer . annual_discounted_price
customer . annual_discounted_price = annual_discounted_price
# Ideally we would have some way to restore flat discounted months
# if we applied discounted to a customer and reverted it but seems
# like an edge case and can be handled manually on request.
2024-04-29 11:16:54 +02:00
customer . flat_discounted_months = 0
2024-05-06 06:12:15 +02:00
customer . save (
update_fields = [
" monthly_discounted_price " ,
" annual_discounted_price " ,
" flat_discounted_months " ,
]
)
2023-10-31 19:22:55 +01:00
plan = get_current_plan_by_customer ( customer )
2024-04-29 11:16:54 +02:00
if plan is not None and plan . tier == customer . required_plan_tier :
2024-05-06 06:12:15 +02:00
self . apply_discount_to_plan ( plan , customer )
2023-12-09 09:21:53 +01:00
2024-04-29 11:16:54 +02:00
# If the customer has a next plan, apply discount to that plan as well.
# Make this a check on CustomerPlan.SWITCH_PLAN_TIER_AT_PLAN_END status
# if we support this for other plans.
next_plan = self . get_legacy_remote_server_next_plan ( customer )
if next_plan is not None and next_plan . tier == customer . required_plan_tier :
2024-05-06 06:12:15 +02:00
self . apply_discount_to_plan ( next_plan , customer )
2023-12-09 09:12:46 +01:00
2023-10-31 19:22:55 +01:00
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . DISCOUNT_CHANGED ,
2023-10-31 19:22:55 +01:00
event_time = timezone_now ( ) ,
2024-05-06 06:12:15 +02:00
extra_data = {
" old_monthly_discounted_price " : old_monthly_discounted_price ,
" new_monthly_discounted_price " : customer . monthly_discounted_price ,
" old_annual_discounted_price " : old_annual_discounted_price ,
" new_annual_discounted_price " : customer . annual_discounted_price ,
} ,
2023-12-21 20:59:38 +01:00
)
2024-05-06 06:12:15 +02:00
return f " Monthly price for { self . billing_entity_display_name } changed to { customer . monthly_discounted_price } from { old_monthly_discounted_price } . Annual price changed to { customer . annual_discounted_price } from { old_annual_discounted_price } . "
2023-10-31 19:22:55 +01:00
2023-12-14 19:55:38 +01:00
def update_customer_minimum_licenses ( self , new_minimum_license_count : int ) - > str :
previous_minimum_license_count = None
customer = self . get_customer ( )
# Currently, the support admin view shows the form for adding
# a minimum license count after a default discount has been set.
assert customer is not None
2024-05-06 06:12:15 +02:00
if not ( customer . monthly_discounted_price or customer . annual_discounted_price ) :
2023-12-14 19:55:38 +01:00
raise SupportRequestError (
f " Discount for { self . billing_entity_display_name } must be updated before setting a minimum number of licenses. "
)
plan = get_current_plan_by_customer ( customer )
if plan is not None and plan . tier != CustomerPlan . TIER_SELF_HOSTED_LEGACY :
raise SupportRequestError (
f " Cannot set minimum licenses; active plan already exists for { self . billing_entity_display_name } . "
)
next_plan = self . get_legacy_remote_server_next_plan ( customer )
2024-01-10 17:21:13 +01:00
if next_plan is not None :
2023-12-14 19:55:38 +01:00
raise SupportRequestError (
f " Cannot set minimum licenses; upgrade to new plan already scheduled for { self . billing_entity_display_name } . "
)
previous_minimum_license_count = customer . minimum_licenses
customer . minimum_licenses = new_minimum_license_count
customer . save ( update_fields = [ " minimum_licenses " ] )
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . CUSTOMER_PROPERTY_CHANGED ,
2023-12-14 19:55:38 +01:00
event_time = timezone_now ( ) ,
extra_data = {
2024-01-12 17:38:55 +01:00
" old_value " : previous_minimum_license_count ,
" new_value " : new_minimum_license_count ,
" property " : " minimum_licenses " ,
2023-12-14 19:55:38 +01:00
} ,
)
if previous_minimum_license_count is None :
previous_minimum_license_count = 0
return f " Minimum licenses for { self . billing_entity_display_name } changed to { new_minimum_license_count } from { previous_minimum_license_count } . "
2024-01-10 17:20:08 +01:00
def set_required_plan_tier ( self , required_plan_tier : int ) - > str :
previous_required_plan_tier = None
new_plan_tier = None
if required_plan_tier != 0 :
new_plan_tier = required_plan_tier
customer = self . get_customer ( )
if new_plan_tier is not None and not self . check_plan_tier_is_billable ( required_plan_tier ) :
raise SupportRequestError ( f " Invalid plan tier for { self . billing_entity_display_name } . " )
if customer is not None :
2024-05-06 06:12:15 +02:00
if new_plan_tier is None and (
customer . monthly_discounted_price or customer . annual_discounted_price
) :
2024-04-29 11:16:54 +02:00
raise SupportRequestError (
f " Discount for { self . billing_entity_display_name } must be 0 before setting required plan tier to None. "
)
2024-01-10 17:20:08 +01:00
previous_required_plan_tier = customer . required_plan_tier
customer . required_plan_tier = new_plan_tier
customer . save ( update_fields = [ " required_plan_tier " ] )
else :
2024-04-29 11:16:54 +02:00
assert new_plan_tier is not None
2024-01-10 17:20:08 +01:00
customer = self . update_or_create_customer (
defaults = { " required_plan_tier " : new_plan_tier }
)
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . CUSTOMER_PROPERTY_CHANGED ,
2024-01-10 17:20:08 +01:00
event_time = timezone_now ( ) ,
extra_data = {
" old_value " : previous_required_plan_tier ,
" new_value " : new_plan_tier ,
" property " : " required_plan_tier " ,
} ,
)
plan_tier_name = " None "
if new_plan_tier is not None :
plan_tier_name = CustomerPlan . name_from_tier ( new_plan_tier )
return f " Required plan tier for { self . billing_entity_display_name } set to { plan_tier_name } . "
2024-08-15 18:29:51 +02:00
def configure_temporary_courtesy_plan ( self , end_date_string : str ) - > str :
plan_end_date = datetime . strptime ( end_date_string , " % Y- % m- %d " ) . replace ( tzinfo = timezone . utc )
if plan_end_date . date ( ) < = timezone_now ( ) . date ( ) :
raise SupportRequestError (
f " Cannot configure a courtesy plan for { self . billing_entity_display_name } to end on { end_date_string } . "
)
customer = self . get_customer ( )
if customer is not None :
plan = get_current_plan_by_customer ( customer )
if plan is not None :
raise SupportRequestError (
f " Cannot configure a courtesy plan for { self . billing_entity_display_name } because of current plan. "
)
plan_anchor_date = timezone_now ( )
if isinstance ( self , RealmBillingSession ) :
raise SupportRequestError (
f " Cannot currently configure a courtesy plan for { self . billing_entity_display_name } . "
) # nocoverage
self . migrate_customer_to_legacy_plan ( plan_anchor_date , plan_end_date )
return f " Temporary courtesy plan for { self . billing_entity_display_name } configured to end on { end_date_string } . "
2024-07-12 02:30:23 +02:00
def configure_fixed_price_plan ( self , fixed_price : int , sent_invoice_id : str | None ) - > str :
2024-01-17 13:55:25 +01:00
customer = self . get_customer ( )
if customer is None :
customer = self . update_or_create_customer ( )
if customer . required_plan_tier is None :
raise SupportRequestError ( " Required plan tier should not be set to None " )
required_plan_tier_name = CustomerPlan . name_from_tier ( customer . required_plan_tier )
fixed_price_cents = fixed_price * 100
2024-07-12 02:30:17 +02:00
fixed_price_plan_params : dict [ str , Any ] = {
2024-01-17 13:55:25 +01:00
" fixed_price " : fixed_price_cents ,
" tier " : customer . required_plan_tier ,
}
current_plan = get_current_plan_by_customer ( customer )
if current_plan is not None and self . check_plan_tier_is_billable ( current_plan . tier ) :
if current_plan . end_date is None :
raise SupportRequestError (
f " Configure { self . billing_entity_display_name } current plan end-date, before scheduling a new plan. "
)
2024-02-15 11:23:42 +01:00
# Handles the case when the current_plan is a fixed-price plan with
# a monthly billing schedule. We can't schedule a new plan until the
# invoice for the 12th month is processed.
if current_plan . end_date != self . get_next_billing_cycle ( current_plan ) :
raise SupportRequestError (
2024-05-20 22:16:21 +02:00
f " New plan for { self . billing_entity_display_name } cannot be scheduled until all the invoices of the current plan are processed. "
2024-02-15 11:23:42 +01:00
)
2024-01-17 13:55:25 +01:00
fixed_price_plan_params [ " billing_cycle_anchor " ] = current_plan . end_date
2024-02-15 11:23:42 +01:00
fixed_price_plan_params [ " end_date " ] = add_months (
current_plan . end_date , CustomerPlan . FIXED_PRICE_PLAN_DURATION_MONTHS
)
2024-01-30 13:41:49 +01:00
fixed_price_plan_params [ " status " ] = CustomerPlan . NEVER_STARTED
2024-01-17 13:55:25 +01:00
fixed_price_plan_params [ " next_invoice_date " ] = current_plan . end_date
2024-01-29 20:29:20 +01:00
fixed_price_plan_params [ " invoicing_status " ] = (
CustomerPlan . INVOICING_STATUS_INITIAL_INVOICE_TO_BE_SENT
)
2024-01-17 13:55:25 +01:00
fixed_price_plan_params [ " billing_schedule " ] = current_plan . billing_schedule
fixed_price_plan_params [ " charge_automatically " ] = current_plan . charge_automatically
# Manual license management is not available for fixed price plan.
fixed_price_plan_params [ " automanage_licenses " ] = True
CustomerPlan . objects . create (
customer = customer ,
* * fixed_price_plan_params ,
)
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . CUSTOMER_PLAN_CREATED ,
2024-01-17 13:55:25 +01:00
event_time = timezone_now ( ) ,
extra_data = fixed_price_plan_params ,
)
current_plan . status = CustomerPlan . SWITCH_PLAN_TIER_AT_PLAN_END
current_plan . next_invoice_date = current_plan . end_date
current_plan . save ( update_fields = [ " status " , " next_invoice_date " ] )
return f " Fixed price { required_plan_tier_name } plan scheduled to start on { current_plan . end_date . date ( ) } . "
2024-02-02 13:04:41 +01:00
if sent_invoice_id is not None :
sent_invoice_id = sent_invoice_id . strip ( )
# Verify 'sent_invoice_id' before storing in database.
try :
invoice = stripe . Invoice . retrieve ( sent_invoice_id )
if invoice . status != " open " :
raise SupportRequestError (
" Invoice status should be open. Please verify sent_invoice_id. "
)
except Exception as e :
raise SupportRequestError ( str ( e ) )
fixed_price_plan_params [ " sent_invoice_id " ] = sent_invoice_id
Invoice . objects . create (
customer = customer ,
stripe_invoice_id = sent_invoice_id ,
status = Invoice . SENT ,
)
2024-01-17 13:55:25 +01:00
fixed_price_plan_params [ " status " ] = CustomerPlanOffer . CONFIGURED
CustomerPlanOffer . objects . create (
customer = customer ,
* * fixed_price_plan_params ,
)
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . CUSTOMER_PLAN_CREATED ,
2024-01-17 13:55:25 +01:00
event_time = timezone_now ( ) ,
extra_data = fixed_price_plan_params ,
)
return f " Customer can now buy a fixed price { required_plan_tier_name } plan. "
2023-11-30 21:11:54 +01:00
def update_customer_sponsorship_status ( self , sponsorship_pending : bool ) - > str :
2023-11-02 18:17:08 +01:00
customer = self . get_customer ( )
if customer is None :
customer = self . update_or_create_customer ( )
customer . sponsorship_pending = sponsorship_pending
customer . save ( update_fields = [ " sponsorship_pending " ] )
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . SPONSORSHIP_PENDING_STATUS_CHANGED ,
2023-11-02 18:17:08 +01:00
event_time = timezone_now ( ) ,
extra_data = { " sponsorship_pending " : sponsorship_pending } ,
)
2023-11-30 21:11:54 +01:00
if sponsorship_pending :
success_message = f " { self . billing_entity_display_name } marked as pending sponsorship. "
else :
success_message = (
f " { self . billing_entity_display_name } is no longer pending sponsorship. "
)
return success_message
2023-12-01 12:23:31 +01:00
def update_billing_modality_of_current_plan ( self , charge_automatically : bool ) - > str :
2023-11-02 18:42:04 +01:00
customer = self . get_customer ( )
if customer is not None :
plan = get_current_plan_by_customer ( customer )
if plan is not None :
plan . charge_automatically = charge_automatically
plan . save ( update_fields = [ " charge_automatically " ] )
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . BILLING_MODALITY_CHANGED ,
2023-11-02 18:42:04 +01:00
event_time = timezone_now ( ) ,
extra_data = { " charge_automatically " : charge_automatically } ,
)
2023-12-01 12:23:31 +01:00
if charge_automatically :
success_message = f " Billing collection method of { self . billing_entity_display_name } updated to charge automatically. "
else :
success_message = f " Billing collection method of { self . billing_entity_display_name } updated to send invoice. "
return success_message
2023-11-02 18:42:04 +01:00
2024-01-08 20:34:16 +01:00
def update_end_date_of_current_plan ( self , end_date_string : str ) - > str :
new_end_date = datetime . strptime ( end_date_string , " % Y- % m- %d " ) . replace ( tzinfo = timezone . utc )
if new_end_date . date ( ) < = timezone_now ( ) . date ( ) :
raise SupportRequestError (
f " Cannot update current plan for { self . billing_entity_display_name } to end on { end_date_string } . "
)
customer = self . get_customer ( )
if customer is not None :
plan = get_current_plan_by_customer ( customer )
if plan is not None :
assert plan . status == CustomerPlan . ACTIVE
2024-01-12 17:38:55 +01:00
old_end_date = plan . end_date
2024-01-08 20:34:16 +01:00
plan . end_date = new_end_date
2024-02-16 11:15:08 +01:00
# Legacy plans should be invoiced once on the end_date to
# downgrade or switch to a new tier.
2024-02-20 07:57:36 +01:00
next_invoice_date_changed_extra_data = None
2024-02-16 11:15:08 +01:00
if plan . tier == CustomerPlan . TIER_SELF_HOSTED_LEGACY :
2024-02-20 07:57:36 +01:00
next_invoice_date_changed_extra_data = {
" old_value " : plan . next_invoice_date ,
" new_value " : new_end_date ,
" property " : " next_invoice_date " ,
}
2024-02-16 11:15:08 +01:00
plan . next_invoice_date = new_end_date
2024-02-15 08:00:20 +01:00
# Currently, we send a reminder email 2 months before the end date.
# Reset it when we are extending the end_date.
reminder_to_review_plan_email_sent_changed_extra_data = None
if (
plan . reminder_to_review_plan_email_sent
and old_end_date is not None # for mypy
and new_end_date > old_end_date
) :
plan . reminder_to_review_plan_email_sent = False
reminder_to_review_plan_email_sent_changed_extra_data = {
" old_value " : True ,
" new_value " : False ,
" plan_id " : plan . id ,
" property " : " reminder_to_review_plan_email_sent " ,
}
plan . save (
update_fields = [
" end_date " ,
" next_invoice_date " ,
" reminder_to_review_plan_email_sent " ,
]
)
2024-02-20 07:57:36 +01:00
2024-07-12 02:30:17 +02:00
def write_to_audit_log_plan_property_changed ( extra_data : dict [ str , Any ] ) - > None :
2024-02-20 07:57:36 +01:00
extra_data [ " plan_id " ] = plan . id
2024-02-16 11:15:08 +01:00
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . CUSTOMER_PLAN_PROPERTY_CHANGED ,
2024-02-16 11:15:08 +01:00
event_time = timezone_now ( ) ,
2024-02-20 07:57:36 +01:00
extra_data = extra_data ,
2024-02-16 11:15:08 +01:00
)
2024-02-20 07:57:36 +01:00
end_date_changed_extra_data = {
" old_value " : old_end_date ,
" new_value " : new_end_date ,
" property " : " end_date " ,
}
write_to_audit_log_plan_property_changed ( end_date_changed_extra_data )
if next_invoice_date_changed_extra_data :
write_to_audit_log_plan_property_changed ( next_invoice_date_changed_extra_data )
2024-02-15 08:00:20 +01:00
if reminder_to_review_plan_email_sent_changed_extra_data :
write_to_audit_log_plan_property_changed (
reminder_to_review_plan_email_sent_changed_extra_data
)
2024-01-08 20:34:16 +01:00
return f " Current plan for { self . billing_entity_display_name } updated to end on { end_date_string } . "
raise SupportRequestError (
f " No current plan for { self . billing_entity_display_name } . "
) # nocoverage
2024-03-04 00:44:59 +01:00
def generate_stripe_invoice (
2023-11-10 14:03:56 +01:00
self ,
plan_tier : int ,
licenses : int ,
license_management : str ,
billing_schedule : int ,
billing_modality : str ,
2024-04-10 10:31:15 +02:00
on_free_trial : bool = False ,
2024-07-12 02:30:23 +02:00
days_until_due : int | None = None ,
current_plan_id : int | None = None ,
2023-11-18 11:29:04 +01:00
) - > str :
2023-11-10 14:03:56 +01:00
customer = self . update_or_create_stripe_customer ( )
assert customer is not None # for mypy
2024-01-17 13:55:25 +01:00
fixed_price_plan_offer = get_configured_fixed_price_plan_offer ( customer , plan_tier )
2023-11-10 14:03:56 +01:00
general_metadata = {
" billing_modality " : billing_modality ,
" billing_schedule " : billing_schedule ,
" licenses " : licenses ,
" license_management " : license_management ,
2024-01-17 13:55:25 +01:00
" price_per_license " : None ,
" fixed_price " : None ,
2023-11-10 14:03:56 +01:00
" type " : " upgrade " ,
2023-12-02 04:21:50 +01:00
" plan_tier " : plan_tier ,
2024-04-10 10:31:15 +02:00
" on_free_trial " : on_free_trial ,
" days_until_due " : days_until_due ,
" current_plan_id " : current_plan_id ,
2023-11-10 14:03:56 +01:00
}
2024-03-30 04:59:59 +01:00
(
invoice_period_start ,
_ ,
invoice_period_end ,
price_per_license ,
) = compute_plan_parameters (
plan_tier ,
billing_schedule ,
2024-05-06 06:12:15 +02:00
customer ,
2024-04-10 10:31:15 +02:00
on_free_trial ,
2024-03-30 04:59:59 +01:00
None ,
not isinstance ( self , RealmBillingSession ) ,
)
2024-01-17 13:55:25 +01:00
if fixed_price_plan_offer is None :
general_metadata [ " price_per_license " ] = price_per_license
else :
general_metadata [ " fixed_price " ] = fixed_price_plan_offer . fixed_price
2024-03-30 04:59:59 +01:00
invoice_period_end = add_months (
invoice_period_start , CustomerPlan . FIXED_PRICE_PLAN_DURATION_MONTHS
)
2024-04-10 10:31:15 +02:00
if on_free_trial and billing_modality == " send_invoice " :
# Paid plan starts at the end of free trial.
invoice_period_start = invoice_period_end
purchased_months = 1
if billing_schedule == CustomerPlan . BILLING_SCHEDULE_ANNUAL :
purchased_months = 12
invoice_period_end = add_months ( invoice_period_end , purchased_months )
2024-03-30 04:59:59 +01:00
general_metadata [ " invoice_period " ] = {
" start " : datetime_to_timestamp ( invoice_period_start ) ,
" end " : datetime_to_timestamp ( invoice_period_end ) ,
}
2024-02-10 07:47:32 +01:00
updated_metadata = self . update_data_for_checkout_session_and_invoice_payment (
2023-11-10 14:03:56 +01:00
general_metadata
)
2024-02-10 07:47:32 +01:00
return self . create_stripe_invoice_and_charge ( updated_metadata )
2023-11-10 14:03:56 +01:00
2023-12-19 12:24:15 +01:00
def ensure_current_plan_is_upgradable ( self , customer : Customer , new_plan_tier : int ) - > None :
2023-12-11 18:00:42 +01:00
# Upgrade for customers with an existing plan is only supported for remote realm / server right now.
if isinstance ( self , RealmBillingSession ) :
2023-12-04 14:20:08 +01:00
ensure_customer_does_not_have_active_plan ( customer )
return
plan = get_current_plan_by_customer ( customer )
2023-12-12 12:42:10 +01:00
# Customers without a plan can always upgrade.
if plan is None :
return
2023-12-04 14:20:08 +01:00
type_of_plan_change = self . get_type_of_plan_tier_change ( plan . tier , new_plan_tier )
2023-12-19 12:24:15 +01:00
if type_of_plan_change != PlanTierChangeType . UPGRADE : # nocoverage
2023-12-05 07:39:21 +01:00
raise InvalidPlanUpgradeError (
2023-12-04 14:20:08 +01:00
f " Cannot upgrade from { plan . name } to { CustomerPlan . name_from_tier ( new_plan_tier ) } "
)
2024-01-03 20:22:49 +01:00
def check_customer_not_on_paid_plan ( self , customer : Customer ) - > str :
2023-12-13 10:21:48 +01:00
current_plan = get_current_plan_by_customer ( customer )
if current_plan is not None :
# Check if the customer is scheduled for an upgrade.
next_plan = self . get_next_plan ( current_plan )
2024-01-03 20:22:49 +01:00
if next_plan is not None : # nocoverage
2023-12-13 10:21:48 +01:00
return f " Customer scheduled for upgrade to { next_plan . name } . Please cancel upgrade before approving sponsorship! "
# It is fine to end legacy plan not scheduled for an upgrade.
if current_plan . tier != CustomerPlan . TIER_SELF_HOSTED_LEGACY :
return f " Customer on plan { current_plan . name } . Please end current plan before approving sponsorship! "
return " "
2023-11-13 07:55:57 +01:00
@catch_stripe_errors
def process_initial_upgrade (
self ,
plan_tier : int ,
licenses : int ,
automanage_licenses : bool ,
billing_schedule : int ,
charge_automatically : bool ,
free_trial : bool ,
2024-07-12 02:30:23 +02:00
remote_server_legacy_plan : CustomerPlan | None = None ,
2023-12-04 14:20:08 +01:00
should_schedule_upgrade_for_legacy_remote_server : bool = False ,
2024-02-02 13:04:41 +01:00
stripe_invoice_paid : bool = False ,
2023-11-13 07:55:57 +01:00
) - > None :
2023-12-11 14:20:13 +01:00
is_self_hosted_billing = not isinstance ( self , RealmBillingSession )
2024-02-02 13:04:41 +01:00
if stripe_invoice_paid :
customer = self . update_or_create_customer ( )
else :
customer = self . update_or_create_stripe_customer ( )
2023-12-04 14:20:08 +01:00
self . ensure_current_plan_is_upgradable ( customer , plan_tier )
billing_cycle_anchor = None
2023-12-11 18:17:41 +01:00
2023-12-19 12:24:15 +01:00
if remote_server_legacy_plan is not None :
2023-12-11 18:17:41 +01:00
# Legacy servers don't get an additional free trial.
free_trial = False
2023-12-19 12:24:15 +01:00
if should_schedule_upgrade_for_legacy_remote_server :
2023-12-04 14:20:08 +01:00
assert remote_server_legacy_plan is not None
billing_cycle_anchor = remote_server_legacy_plan . end_date
2024-01-17 13:55:25 +01:00
fixed_price_plan_offer = get_configured_fixed_price_plan_offer ( customer , plan_tier )
2024-05-06 06:12:15 +02:00
if fixed_price_plan_offer is not None :
2024-01-17 13:55:25 +01:00
assert automanage_licenses is True
2023-11-13 07:55:57 +01:00
(
billing_cycle_anchor ,
next_invoice_date ,
period_end ,
price_per_license ,
) = compute_plan_parameters (
plan_tier ,
billing_schedule ,
2024-05-06 06:12:15 +02:00
customer ,
2023-11-13 07:55:57 +01:00
free_trial ,
2023-12-04 14:20:08 +01:00
billing_cycle_anchor ,
2023-12-11 14:20:13 +01:00
is_self_hosted_billing ,
2024-01-22 11:20:05 +01:00
should_schedule_upgrade_for_legacy_remote_server ,
2023-11-13 07:55:57 +01:00
)
# TODO: The correctness of this relies on user creation, deactivation, etc being
# in a transaction.atomic() with the relevant RealmAuditLog entries
with transaction . atomic ( ) :
2023-12-12 14:18:38 +01:00
# billed_licenses can be greater than licenses if users are added between the start of
# this function (process_initial_upgrade) and now
current_licenses_count = self . get_billable_licenses_for_customer (
customer , plan_tier , licenses
)
2023-12-12 19:22:51 +01:00
# In case user wants more licenses for the plan. (manual license management)
2023-12-12 14:18:38 +01:00
billed_licenses = max ( current_licenses_count , licenses )
2023-11-13 07:55:57 +01:00
plan_params = {
" automanage_licenses " : automanage_licenses ,
" charge_automatically " : charge_automatically ,
" billing_cycle_anchor " : billing_cycle_anchor ,
" billing_schedule " : billing_schedule ,
" tier " : plan_tier ,
}
2024-01-17 13:55:25 +01:00
if fixed_price_plan_offer is None :
plan_params [ " price_per_license " ] = price_per_license
2024-05-06 06:12:15 +02:00
_price_per_license , percent_off = get_price_per_license_and_discount (
plan_tier , billing_schedule , customer
)
plan_params [ " discount " ] = percent_off
assert price_per_license == _price_per_license
2024-01-17 13:55:25 +01:00
2023-11-13 07:55:57 +01:00
if free_trial :
plan_params [ " status " ] = CustomerPlan . FREE_TRIAL
2023-11-18 11:29:04 +01:00
if charge_automatically :
# Ensure free trial customers not paying via invoice have a default payment method set
2024-02-02 13:04:41 +01:00
assert customer . stripe_customer_id is not None # for mypy
2023-11-18 11:29:04 +01:00
stripe_customer = stripe_get_customer ( customer . stripe_customer_id )
if not stripe_customer_has_credit_card_as_default_payment_method (
stripe_customer
) :
raise BillingError (
" no payment method " ,
_ ( " Please add a credit card before starting your free trial. " ) ,
)
2023-12-04 14:20:08 +01:00
event_time = billing_cycle_anchor
2023-12-19 12:24:15 +01:00
if should_schedule_upgrade_for_legacy_remote_server :
2023-12-04 14:20:08 +01:00
# In this code path, we are currently on a legacy plan
# and are scheduling an upgrade to a non-legacy plan
# that should occur when the legacy plan expires.
#
# We will create a new NEVER_STARTED plan for the
# customer, scheduled to start when the current one
# expires.
assert remote_server_legacy_plan is not None
if charge_automatically :
# Ensure customers not paying via invoice have a default payment method set.
2024-02-02 13:04:41 +01:00
assert customer . stripe_customer_id is not None # for mypy
2023-12-04 14:20:08 +01:00
stripe_customer = stripe_get_customer ( customer . stripe_customer_id )
if not stripe_customer_has_credit_card_as_default_payment_method (
stripe_customer
2023-12-19 12:24:15 +01:00
) : # nocoverage
2023-12-04 14:20:08 +01:00
raise BillingError (
" no payment method " ,
_ ( " Please add a credit card to schedule upgrade. " ) ,
)
# Settings status > CustomerPLan.LIVE_STATUS_THRESHOLD makes sure we don't have
# to worry about this plan being used for any other purpose.
# NOTE: This is the 2nd plan for the customer.
plan_params [ " status " ] = CustomerPlan . NEVER_STARTED
2024-01-29 00:32:21 +01:00
plan_params [ " invoicing_status " ] = (
CustomerPlan . INVOICING_STATUS_INITIAL_INVOICE_TO_BE_SENT
)
2023-12-04 14:20:08 +01:00
event_time = timezone_now ( ) . replace ( microsecond = 0 )
# Schedule switching to the new plan at plan end date.
assert remote_server_legacy_plan . end_date == billing_cycle_anchor
2023-12-12 14:51:23 +01:00
last_ledger_entry = (
LicenseLedger . objects . filter ( plan = remote_server_legacy_plan )
. order_by ( " -id " )
. first ( )
)
# Update license_at_next_renewal as per new plan.
assert last_ledger_entry is not None
2023-12-12 19:22:51 +01:00
last_ledger_entry . licenses_at_next_renewal = billed_licenses
2023-12-12 14:51:23 +01:00
last_ledger_entry . save ( update_fields = [ " licenses_at_next_renewal " ] )
2023-12-04 14:20:08 +01:00
remote_server_legacy_plan . status = CustomerPlan . SWITCH_PLAN_TIER_AT_PLAN_END
2023-12-06 07:44:11 +01:00
remote_server_legacy_plan . save ( update_fields = [ " status " ] )
2023-12-04 14:20:08 +01:00
elif remote_server_legacy_plan is not None : # nocoverage
remote_server_legacy_plan . status = CustomerPlan . ENDED
remote_server_legacy_plan . save ( update_fields = [ " status " ] )
2024-01-17 13:55:25 +01:00
if fixed_price_plan_offer is not None :
# Manual license management is not available for fixed price plan.
assert automanage_licenses is True
plan_params [ " fixed_price " ] = fixed_price_plan_offer . fixed_price
2024-03-30 04:59:59 +01:00
period_end = add_months (
2024-02-15 11:23:42 +01:00
billing_cycle_anchor , CustomerPlan . FIXED_PRICE_PLAN_DURATION_MONTHS
)
2024-03-30 04:59:59 +01:00
plan_params [ " end_date " ] = period_end
2024-01-17 13:55:25 +01:00
fixed_price_plan_offer . status = CustomerPlanOffer . PROCESSED
fixed_price_plan_offer . save ( update_fields = [ " status " ] )
2023-11-13 07:55:57 +01:00
plan = CustomerPlan . objects . create (
customer = customer , next_invoice_date = next_invoice_date , * * plan_params
)
2023-12-06 07:43:18 +01:00
2024-03-03 14:21:17 +01:00
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . CUSTOMER_PLAN_CREATED ,
2024-03-03 14:21:17 +01:00
event_time = event_time ,
extra_data = plan_params ,
)
2023-12-06 07:43:18 +01:00
if plan . status < CustomerPlan . LIVE_STATUS_THRESHOLD :
2024-03-03 14:21:17 +01:00
# Tier and usage limit change will happen when plan becomes live.
self . do_change_plan_type ( tier = plan_tier )
2023-12-06 07:43:18 +01:00
# LicenseLedger entries are way for us to charge customer and track their license usage.
# So, we should only create these entries for live plans.
ledger_entry = LicenseLedger . objects . create (
plan = plan ,
is_renewal = True ,
event_time = event_time ,
2024-02-10 07:47:32 +01:00
licenses = licenses ,
licenses_at_next_renewal = licenses ,
2023-12-06 07:43:18 +01:00
)
plan . invoiced_through = ledger_entry
plan . save ( update_fields = [ " invoiced_through " ] )
2024-02-10 07:47:32 +01:00
# TODO: Do a check for max licenses for fixed price plans here after we add that.
if (
stripe_invoice_paid
and billed_licenses != licenses
and not customer . exempt_from_license_number_check
and not fixed_price_plan_offer
) :
# Customer paid for less licenses than they have.
# We need to create a new ledger entry to track the additional licenses.
LicenseLedger . objects . create (
plan = plan ,
is_renewal = False ,
event_time = event_time ,
licenses = billed_licenses ,
licenses_at_next_renewal = billed_licenses ,
)
# Creates due today invoice for additional licenses.
self . invoice_plan ( plan , event_time )
2024-02-02 13:04:41 +01:00
if not stripe_invoice_paid and not (
free_trial or should_schedule_upgrade_for_legacy_remote_server
) :
2024-04-10 10:31:15 +02:00
# We don't actually expect to ever reach here but this is just a safety net
# in case any future changes make this possible.
2023-12-04 14:20:08 +01:00
assert plan is not None
2024-02-10 07:47:32 +01:00
self . generate_invoice_for_upgrade (
customer ,
price_per_license = price_per_license ,
fixed_price = plan . fixed_price ,
licenses = billed_licenses ,
plan_tier = plan . tier ,
billing_schedule = billing_schedule ,
charge_automatically = False ,
2024-03-30 04:59:59 +01:00
invoice_period = {
" start " : datetime_to_timestamp ( billing_cycle_anchor ) ,
" end " : datetime_to_timestamp ( period_end ) ,
} ,
2023-11-13 07:55:57 +01:00
)
2024-04-10 10:31:15 +02:00
elif free_trial and not charge_automatically :
assert stripe_invoice_paid is False
assert plan is not None
assert plan . next_invoice_date is not None
# Send an invoice to the customer which expires at the end of free trial. If the customer
# fails to pay the invoice before expiration, we downgrade the customer.
self . generate_stripe_invoice (
plan_tier ,
licenses = billed_licenses ,
license_management = " automatic " if automanage_licenses else " manual " ,
billing_schedule = billing_schedule ,
billing_modality = " send_invoice " ,
on_free_trial = True ,
days_until_due = ( plan . next_invoice_date - event_time ) . days ,
current_plan_id = plan . id ,
)
2023-11-13 07:55:57 +01:00
2024-07-12 02:30:17 +02:00
def do_upgrade ( self , upgrade_request : UpgradeRequest ) - > dict [ str , Any ] :
2023-11-14 11:59:48 +01:00
customer = self . get_customer ( )
if customer is not None :
2023-12-04 14:20:08 +01:00
self . ensure_current_plan_is_upgradable ( customer , upgrade_request . tier )
2023-11-14 11:59:48 +01:00
billing_modality = upgrade_request . billing_modality
schedule = upgrade_request . schedule
license_management = upgrade_request . license_management
licenses = upgrade_request . licenses
seat_count = unsign_seat_count ( upgrade_request . signed_seat_count , upgrade_request . salt )
if billing_modality == " charge_automatically " and license_management == " automatic " :
licenses = seat_count
if billing_modality == " send_invoice " :
license_management = " manual "
exempt_from_license_number_check = (
customer is not None and customer . exempt_from_license_number_check
)
check_upgrade_parameters (
billing_modality ,
schedule ,
license_management ,
licenses ,
seat_count ,
exempt_from_license_number_check ,
2023-12-09 08:16:53 +01:00
self . min_licenses_for_plan ( upgrade_request . tier ) ,
2023-11-14 11:59:48 +01:00
)
assert licenses is not None and license_management is not None
automanage_licenses = license_management == " automatic "
charge_automatically = billing_modality == " charge_automatically "
2023-11-30 07:55:53 +01:00
billing_schedule = {
" annual " : CustomerPlan . BILLING_SCHEDULE_ANNUAL ,
" monthly " : CustomerPlan . BILLING_SCHEDULE_MONTHLY ,
} [ schedule ]
2024-07-12 02:30:17 +02:00
data : dict [ str , Any ] = { }
2024-01-31 19:20:52 +01:00
2023-12-11 14:20:13 +01:00
is_self_hosted_billing = not isinstance ( self , RealmBillingSession )
2024-01-07 05:58:39 +01:00
free_trial = is_free_trial_offer_enabled ( is_self_hosted_billing , upgrade_request . tier )
2024-01-31 19:20:52 +01:00
if customer is not None :
fixed_price_plan_offer = get_configured_fixed_price_plan_offer (
customer , upgrade_request . tier
)
if fixed_price_plan_offer is not None :
free_trial = False
2024-04-08 11:58:02 +02:00
if self . customer_plan_exists ( ) :
# Free trial is not available for existing customers.
2024-03-15 03:03:11 +01:00
free_trial = False
2024-02-22 05:30:41 +01:00
2023-12-04 14:20:08 +01:00
remote_server_legacy_plan = self . get_remote_server_legacy_plan ( customer )
should_schedule_upgrade_for_legacy_remote_server = (
remote_server_legacy_plan is not None
and upgrade_request . remote_server_plan_start_date == " billing_cycle_end_date "
)
2023-11-18 11:29:04 +01:00
# Directly upgrade free trial orgs or invoice payment orgs to standard plan.
2024-03-04 00:44:59 +01:00
if should_schedule_upgrade_for_legacy_remote_server or free_trial :
2023-11-18 11:29:04 +01:00
self . process_initial_upgrade (
2023-12-02 04:21:50 +01:00
upgrade_request . tier ,
2023-11-14 11:59:48 +01:00
licenses ,
2023-11-18 11:29:04 +01:00
automanage_licenses ,
2023-11-14 11:59:48 +01:00
billing_schedule ,
2023-11-18 11:29:04 +01:00
charge_automatically ,
2023-12-11 14:20:13 +01:00
free_trial ,
2023-12-04 14:20:08 +01:00
remote_server_legacy_plan ,
should_schedule_upgrade_for_legacy_remote_server ,
2023-11-14 11:59:48 +01:00
)
2023-11-18 11:29:04 +01:00
data [ " organization_upgrade_successful " ] = True
2023-11-14 11:59:48 +01:00
else :
2024-03-04 00:44:59 +01:00
stripe_invoice_id = self . generate_stripe_invoice (
2023-12-02 04:21:50 +01:00
upgrade_request . tier ,
2023-11-14 11:59:48 +01:00
licenses ,
2023-11-18 11:29:04 +01:00
license_management ,
2023-11-14 11:59:48 +01:00
billing_schedule ,
2023-11-18 11:29:04 +01:00
billing_modality ,
2023-11-14 11:59:48 +01:00
)
2024-02-10 07:47:32 +01:00
data [ " stripe_invoice_id " ] = stripe_invoice_id
2023-11-14 11:59:48 +01:00
return data
2023-11-26 15:41:28 +01:00
def do_change_schedule_after_free_trial ( self , plan : CustomerPlan , schedule : int ) - > None :
2024-04-10 10:31:15 +02:00
# NOTE: Schedule change for free trial with invoice payments is not supported due to complication
# involving sending another invoice and handling payment difference if customer already paid.
assert plan . charge_automatically
2023-11-26 15:41:28 +01:00
# Change the billing frequency of the plan after the free trial ends.
2023-11-30 07:55:53 +01:00
assert schedule in (
CustomerPlan . BILLING_SCHEDULE_MONTHLY ,
CustomerPlan . BILLING_SCHEDULE_ANNUAL ,
)
2023-11-26 15:41:28 +01:00
last_ledger_entry = LicenseLedger . objects . filter ( plan = plan ) . order_by ( " -id " ) . first ( )
assert last_ledger_entry is not None
licenses_at_next_renewal = last_ledger_entry . licenses_at_next_renewal
assert licenses_at_next_renewal is not None
assert plan . next_invoice_date is not None
next_billing_cycle = plan . next_invoice_date
if plan . fixed_price is not None : # nocoverage
raise BillingError ( " Customer is already on monthly fixed plan. " )
plan . status = CustomerPlan . ENDED
2024-03-01 14:27:06 +01:00
plan . next_invoice_date = None
plan . save ( update_fields = [ " status " , " next_invoice_date " ] )
2023-11-26 15:41:28 +01:00
2024-05-06 06:12:15 +02:00
price_per_license , discount_for_current_plan = get_price_per_license_and_discount (
plan . tier , schedule , plan . customer
2023-11-26 15:41:28 +01:00
)
new_plan = CustomerPlan . objects . create (
customer = plan . customer ,
billing_schedule = schedule ,
automanage_licenses = plan . automanage_licenses ,
charge_automatically = plan . charge_automatically ,
price_per_license = price_per_license ,
2024-01-10 14:59:49 +01:00
discount = discount_for_current_plan ,
2023-11-26 15:41:28 +01:00
billing_cycle_anchor = plan . billing_cycle_anchor ,
tier = plan . tier ,
status = CustomerPlan . FREE_TRIAL ,
next_invoice_date = next_billing_cycle ,
)
2024-03-01 14:27:06 +01:00
ledger_entry = LicenseLedger . objects . create (
2023-11-26 15:41:28 +01:00
plan = new_plan ,
is_renewal = True ,
event_time = plan . billing_cycle_anchor ,
licenses = licenses_at_next_renewal ,
licenses_at_next_renewal = licenses_at_next_renewal ,
)
2024-03-01 14:27:06 +01:00
new_plan . invoiced_through = ledger_entry
new_plan . save ( update_fields = [ " invoiced_through " ] )
2023-11-30 07:55:53 +01:00
if schedule == CustomerPlan . BILLING_SCHEDULE_ANNUAL :
2023-11-26 15:41:28 +01:00
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN ,
2023-11-26 15:41:28 +01:00
event_time = timezone_now ( ) ,
extra_data = {
" monthly_plan_id " : plan . id ,
" annual_plan_id " : new_plan . id ,
} ,
)
else :
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN ,
2023-11-26 15:41:28 +01:00
event_time = timezone_now ( ) ,
extra_data = {
" annual_plan_id " : plan . id ,
" monthly_plan_id " : new_plan . id ,
} ,
)
2023-11-26 11:33:50 +01:00
def get_next_billing_cycle ( self , plan : CustomerPlan ) - > datetime :
2023-11-25 14:44:47 +01:00
if plan . status in (
CustomerPlan . FREE_TRIAL ,
CustomerPlan . DOWNGRADE_AT_END_OF_FREE_TRIAL ,
2024-03-15 18:50:09 +01:00
CustomerPlan . NEVER_STARTED ,
2023-11-25 14:44:47 +01:00
) :
2023-11-13 15:05:56 +01:00
assert plan . next_invoice_date is not None
next_billing_cycle = plan . next_invoice_date
2023-12-19 12:24:15 +01:00
elif plan . status == CustomerPlan . SWITCH_PLAN_TIER_AT_PLAN_END :
2023-12-04 14:20:08 +01:00
assert plan . end_date is not None
next_billing_cycle = plan . end_date
2023-11-13 15:05:56 +01:00
else :
2024-03-15 18:50:09 +01:00
last_ledger_renewal = (
LicenseLedger . objects . filter ( plan = plan , is_renewal = True ) . order_by ( " -id " ) . first ( )
)
assert last_ledger_renewal is not None
last_renewal = last_ledger_renewal . event_time
2023-11-13 15:05:56 +01:00
next_billing_cycle = start_of_next_billing_cycle ( plan , last_renewal )
2023-11-26 11:33:50 +01:00
2024-01-19 15:48:43 +01:00
if plan . end_date is not None :
next_billing_cycle = min ( next_billing_cycle , plan . end_date )
2023-11-26 11:33:50 +01:00
return next_billing_cycle
# event_time should roughly be timezone_now(). Not designed to handle
# event_times in the past or future
@transaction.atomic
def make_end_of_cycle_updates_if_needed (
self , plan : CustomerPlan , event_time : datetime
2024-07-12 02:30:23 +02:00
) - > tuple [ CustomerPlan | None , LicenseLedger | None ] :
2024-03-07 09:55:00 +01:00
last_ledger_entry = (
LicenseLedger . objects . filter ( plan = plan , event_time__lte = event_time )
. order_by ( " -id " )
. first ( )
)
2023-11-26 11:33:50 +01:00
next_billing_cycle = self . get_next_billing_cycle ( plan )
2023-11-26 15:41:28 +01:00
event_in_next_billing_cycle = next_billing_cycle < = event_time
2023-11-26 11:33:50 +01:00
2023-11-26 15:41:28 +01:00
if event_in_next_billing_cycle and last_ledger_entry is not None :
2023-11-13 15:05:56 +01:00
licenses_at_next_renewal = last_ledger_entry . licenses_at_next_renewal
assert licenses_at_next_renewal is not None
2024-01-19 15:48:43 +01:00
2024-02-15 11:23:42 +01:00
if plan . end_date == next_billing_cycle and plan . status == CustomerPlan . ACTIVE :
2024-01-19 15:48:43 +01:00
self . process_downgrade ( plan , True )
return None , None
2023-11-13 15:05:56 +01:00
if plan . status == CustomerPlan . ACTIVE :
return None , LicenseLedger . objects . create (
plan = plan ,
is_renewal = True ,
event_time = next_billing_cycle ,
licenses = licenses_at_next_renewal ,
licenses_at_next_renewal = licenses_at_next_renewal ,
)
if plan . is_free_trial ( ) :
2024-04-10 10:31:15 +02:00
is_renewal = True
# Check if user has already paid for the plan by invoice.
if not plan . charge_automatically :
last_sent_invoice = Invoice . objects . filter ( plan = plan ) . order_by ( " -id " ) . first ( )
if last_sent_invoice and last_sent_invoice . status == Invoice . PAID :
# This will create invoice for any additional licenses that user has at the time of
# switching from free trial to paid plan since they already paid for the plan's this billing cycle.
is_renewal = False
else :
# We end the free trial since customer hasn't paid.
plan . status = CustomerPlan . DOWNGRADE_AT_END_OF_FREE_TRIAL
plan . save ( update_fields = [ " status " ] )
self . make_end_of_cycle_updates_if_needed ( plan , event_time )
return None , None
2023-11-13 15:05:56 +01:00
plan . invoiced_through = last_ledger_entry
plan . billing_cycle_anchor = next_billing_cycle . replace ( microsecond = 0 )
plan . status = CustomerPlan . ACTIVE
plan . save ( update_fields = [ " invoiced_through " , " billing_cycle_anchor " , " status " ] )
return None , LicenseLedger . objects . create (
plan = plan ,
2024-04-10 10:31:15 +02:00
is_renewal = is_renewal ,
2023-11-13 15:05:56 +01:00
event_time = next_billing_cycle ,
licenses = licenses_at_next_renewal ,
licenses_at_next_renewal = licenses_at_next_renewal ,
)
2023-12-04 14:20:08 +01:00
if plan . status == CustomerPlan . SWITCH_PLAN_TIER_AT_PLAN_END : # nocoverage
plan . status = CustomerPlan . ENDED
plan . save ( update_fields = [ " status " ] )
assert plan . end_date is not None
new_plan = CustomerPlan . objects . get (
customer = plan . customer ,
billing_cycle_anchor = plan . end_date ,
status = CustomerPlan . NEVER_STARTED ,
)
new_plan . status = CustomerPlan . ACTIVE
new_plan . save ( update_fields = [ " status " ] )
2023-12-24 15:56:33 +01:00
self . do_change_plan_type ( tier = new_plan . tier , background_update = True )
2023-12-04 14:20:08 +01:00
return None , LicenseLedger . objects . create (
plan = new_plan ,
is_renewal = True ,
event_time = next_billing_cycle ,
licenses = licenses_at_next_renewal ,
licenses_at_next_renewal = licenses_at_next_renewal ,
)
2023-11-13 15:05:56 +01:00
if plan . status == CustomerPlan . SWITCH_TO_ANNUAL_AT_END_OF_CYCLE :
if plan . fixed_price is not None : # nocoverage
raise NotImplementedError ( " Can ' t switch fixed priced monthly plan to annual. " )
plan . status = CustomerPlan . ENDED
plan . save ( update_fields = [ " status " ] )
2024-05-06 06:12:15 +02:00
price_per_license , discount_for_current_plan = get_price_per_license_and_discount (
plan . tier , CustomerPlan . BILLING_SCHEDULE_ANNUAL , plan . customer
2023-11-13 15:05:56 +01:00
)
new_plan = CustomerPlan . objects . create (
customer = plan . customer ,
2023-11-30 07:55:53 +01:00
billing_schedule = CustomerPlan . BILLING_SCHEDULE_ANNUAL ,
2023-11-13 15:05:56 +01:00
automanage_licenses = plan . automanage_licenses ,
charge_automatically = plan . charge_automatically ,
price_per_license = price_per_license ,
2024-01-10 14:59:49 +01:00
discount = discount_for_current_plan ,
2023-11-13 15:05:56 +01:00
billing_cycle_anchor = next_billing_cycle ,
tier = plan . tier ,
status = CustomerPlan . ACTIVE ,
next_invoice_date = next_billing_cycle ,
invoiced_through = None ,
2023-11-30 08:07:12 +01:00
invoicing_status = CustomerPlan . INVOICING_STATUS_INITIAL_INVOICE_TO_BE_SENT ,
2023-11-13 15:05:56 +01:00
)
new_plan_ledger_entry = LicenseLedger . objects . create (
plan = new_plan ,
is_renewal = True ,
event_time = next_billing_cycle ,
licenses = licenses_at_next_renewal ,
licenses_at_next_renewal = licenses_at_next_renewal ,
)
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN ,
2023-11-13 15:05:56 +01:00
event_time = event_time ,
extra_data = {
" monthly_plan_id " : plan . id ,
" annual_plan_id " : new_plan . id ,
} ,
2023-12-24 15:56:33 +01:00
background_update = True ,
2023-11-13 15:05:56 +01:00
)
return new_plan , new_plan_ledger_entry
2023-11-20 13:01:25 +01:00
if plan . status == CustomerPlan . SWITCH_TO_MONTHLY_AT_END_OF_CYCLE :
if plan . fixed_price is not None : # nocoverage
raise BillingError ( " Customer is already on monthly fixed plan. " )
plan . status = CustomerPlan . ENDED
plan . save ( update_fields = [ " status " ] )
2024-05-06 06:12:15 +02:00
price_per_license , discount_for_current_plan = get_price_per_license_and_discount (
plan . tier , CustomerPlan . BILLING_SCHEDULE_MONTHLY , plan . customer
2023-11-20 13:01:25 +01:00
)
new_plan = CustomerPlan . objects . create (
customer = plan . customer ,
2023-11-30 07:55:53 +01:00
billing_schedule = CustomerPlan . BILLING_SCHEDULE_MONTHLY ,
2023-11-20 13:01:25 +01:00
automanage_licenses = plan . automanage_licenses ,
charge_automatically = plan . charge_automatically ,
price_per_license = price_per_license ,
2024-01-10 14:59:49 +01:00
discount = discount_for_current_plan ,
2023-11-20 13:01:25 +01:00
billing_cycle_anchor = next_billing_cycle ,
tier = plan . tier ,
status = CustomerPlan . ACTIVE ,
next_invoice_date = next_billing_cycle ,
invoiced_through = None ,
2023-11-30 08:07:12 +01:00
invoicing_status = CustomerPlan . INVOICING_STATUS_INITIAL_INVOICE_TO_BE_SENT ,
2023-11-20 13:01:25 +01:00
)
new_plan_ledger_entry = LicenseLedger . objects . create (
plan = new_plan ,
is_renewal = True ,
event_time = next_billing_cycle ,
licenses = licenses_at_next_renewal ,
licenses_at_next_renewal = licenses_at_next_renewal ,
)
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN ,
2023-11-20 13:01:25 +01:00
event_time = event_time ,
extra_data = {
" annual_plan_id " : plan . id ,
" monthly_plan_id " : new_plan . id ,
} ,
2023-12-24 15:56:33 +01:00
background_update = True ,
2023-11-20 13:01:25 +01:00
)
return new_plan , new_plan_ledger_entry
2023-11-25 14:44:47 +01:00
if plan . status == CustomerPlan . DOWNGRADE_AT_END_OF_FREE_TRIAL :
2023-12-24 15:56:33 +01:00
self . downgrade_now_without_creating_additional_invoices (
plan , background_update = True
)
2023-11-25 14:44:47 +01:00
2023-11-13 15:05:56 +01:00
if plan . status == CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE :
2023-12-24 15:56:33 +01:00
self . process_downgrade ( plan , background_update = True )
2023-11-26 15:41:28 +01:00
2023-11-13 15:05:56 +01:00
return None , None
return None , last_ledger_entry
2024-07-12 02:30:23 +02:00
def get_next_plan ( self , plan : CustomerPlan ) - > CustomerPlan | None :
2023-12-06 08:21:13 +01:00
if plan . status == CustomerPlan . SWITCH_PLAN_TIER_AT_PLAN_END :
assert plan . end_date is not None
return CustomerPlan . objects . filter (
2024-01-31 14:10:45 +01:00
customer = plan . customer ,
2023-12-06 08:21:13 +01:00
billing_cycle_anchor = plan . end_date ,
status = CustomerPlan . NEVER_STARTED ,
) . first ( )
return None
2024-05-22 16:47:29 +02:00
def get_annual_recurring_revenue_for_support_data (
self , plan : CustomerPlan , last_ledger_entry : LicenseLedger
) - > int :
if plan . fixed_price is not None :
# For support and activity views, we want to show the annual
# revenue for the currently configured fixed price, which
# is the annual amount charged in cents.
return plan . fixed_price
revenue = self . get_customer_plan_renewal_amount ( plan , last_ledger_entry )
if plan . billing_schedule == CustomerPlan . BILLING_SCHEDULE_MONTHLY :
revenue * = 12
return revenue
2023-12-12 19:45:46 +01:00
def get_customer_plan_renewal_amount (
self ,
plan : CustomerPlan ,
2024-01-22 17:12:11 +01:00
last_ledger_entry : LicenseLedger ,
2023-12-12 19:45:46 +01:00
) - > int :
if plan . fixed_price is not None :
2024-02-28 08:32:49 +01:00
if plan . end_date == self . get_next_billing_cycle ( plan ) :
return 0
2024-01-17 13:55:25 +01:00
return get_amount_due_fixed_price_plan ( plan . fixed_price , plan . billing_schedule )
2023-12-12 19:45:46 +01:00
if last_ledger_entry . licenses_at_next_renewal is None :
return 0 # nocoverage
assert plan . price_per_license is not None # for mypy
return plan . price_per_license * last_ledger_entry . licenses_at_next_renewal
2023-12-06 07:09:33 +01:00
def get_billing_context_from_plan (
self ,
customer : Customer ,
plan : CustomerPlan ,
last_ledger_entry : LicenseLedger ,
now : datetime ,
2024-07-12 02:30:17 +02:00
) - > dict [ str , Any ] :
2024-04-10 10:31:15 +02:00
is_self_hosted_billing = not isinstance ( self , RealmBillingSession )
2023-12-06 06:19:57 +01:00
downgrade_at_end_of_cycle = plan . status == CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE
downgrade_at_end_of_free_trial = plan . status == CustomerPlan . DOWNGRADE_AT_END_OF_FREE_TRIAL
switch_to_annual_at_end_of_cycle = (
plan . status == CustomerPlan . SWITCH_TO_ANNUAL_AT_END_OF_CYCLE
)
switch_to_monthly_at_end_of_cycle = (
plan . status == CustomerPlan . SWITCH_TO_MONTHLY_AT_END_OF_CYCLE
)
licenses = last_ledger_entry . licenses
licenses_at_next_renewal = last_ledger_entry . licenses_at_next_renewal
assert licenses_at_next_renewal is not None
2023-12-09 08:16:53 +01:00
min_licenses_for_plan = self . min_licenses_for_plan ( plan . tier )
2023-12-06 06:19:57 +01:00
seat_count = self . current_count_for_billed_licenses ( )
2023-12-09 08:16:53 +01:00
using_min_licenses_for_plan = (
min_licenses_for_plan == licenses_at_next_renewal
and licenses_at_next_renewal > seat_count
)
2023-12-06 06:19:57 +01:00
# Should do this in JavaScript, using the user's time zone
if plan . is_free_trial ( ) or downgrade_at_end_of_free_trial :
assert plan . next_invoice_date is not None
2024-03-21 03:43:05 +01:00
renewal_date = f " { plan . next_invoice_date : %B } { plan . next_invoice_date . day } , { plan . next_invoice_date . year } "
2023-12-06 06:19:57 +01:00
else :
renewal_date = " { dt: % B} {dt.day} , {dt.year} " . format (
dt = start_of_next_billing_cycle ( plan , now )
2023-12-06 06:14:32 +01:00
)
2023-12-06 06:19:57 +01:00
2024-04-10 10:31:15 +02:00
has_paid_invoice_for_free_trial = False
free_trial_next_renewal_date_after_invoice_paid = None
if plan . is_free_trial ( ) and not plan . charge_automatically :
last_sent_invoice = Invoice . objects . filter ( plan = plan ) . order_by ( " -id " ) . first ( )
# If the customer doesn't have any invoice, this likely means a bug and customer needs to be handled manually.
assert last_sent_invoice is not None
has_paid_invoice_for_free_trial = last_sent_invoice . status == Invoice . PAID
if has_paid_invoice_for_free_trial :
assert plan . next_invoice_date is not None
free_trial_days = get_free_trial_days ( is_self_hosted_billing , plan . tier )
assert free_trial_days is not None
free_trial_next_renewal_date_after_invoice_paid = (
" { dt: % B} {dt.day} , {dt.year} " . format (
dt = (
start_of_next_billing_cycle ( plan , plan . next_invoice_date )
+ timedelta ( days = free_trial_days )
)
)
)
2023-12-06 06:19:57 +01:00
billing_frequency = CustomerPlan . BILLING_SCHEDULES [ plan . billing_schedule ]
if switch_to_annual_at_end_of_cycle :
2023-12-20 07:24:21 +01:00
num_months_next_cycle = 12
2023-12-06 06:19:57 +01:00
annual_price_per_license = get_price_per_license (
2024-05-06 06:12:15 +02:00
plan . tier , CustomerPlan . BILLING_SCHEDULE_ANNUAL , customer
2023-12-06 06:14:32 +01:00
)
2023-12-06 06:19:57 +01:00
renewal_cents = annual_price_per_license * licenses_at_next_renewal
price_per_license = format_money ( annual_price_per_license / 12 )
elif switch_to_monthly_at_end_of_cycle :
2023-12-20 07:24:21 +01:00
num_months_next_cycle = 1
2023-12-06 06:19:57 +01:00
monthly_price_per_license = get_price_per_license (
2024-05-06 06:12:15 +02:00
plan . tier , CustomerPlan . BILLING_SCHEDULE_MONTHLY , customer
2023-12-06 06:14:32 +01:00
)
2023-12-06 06:19:57 +01:00
renewal_cents = monthly_price_per_license * licenses_at_next_renewal
price_per_license = format_money ( monthly_price_per_license )
else :
2023-12-20 07:24:21 +01:00
num_months_next_cycle = (
12 if plan . billing_schedule == CustomerPlan . BILLING_SCHEDULE_ANNUAL else 1
)
2024-01-22 17:12:11 +01:00
renewal_cents = self . get_customer_plan_renewal_amount ( plan , last_ledger_entry )
2023-12-06 06:14:32 +01:00
2023-12-06 06:19:57 +01:00
if plan . price_per_license is None :
price_per_license = " "
elif billing_frequency == " Annual " :
price_per_license = format_money ( plan . price_per_license / 12 )
2023-12-06 06:14:32 +01:00
else :
2023-12-06 06:19:57 +01:00
price_per_license = format_money ( plan . price_per_license )
2023-12-06 06:14:32 +01:00
2023-12-20 07:24:21 +01:00
pre_discount_renewal_cents = renewal_cents
flat_discount , flat_discounted_months = self . get_flat_discount_info ( plan . customer )
2024-01-17 13:55:25 +01:00
if plan . fixed_price is None and flat_discounted_months > 0 :
2023-12-20 07:24:21 +01:00
flat_discounted_months = min ( flat_discounted_months , num_months_next_cycle )
discount = flat_discount * flat_discounted_months
2024-07-14 21:06:04 +02:00
renewal_cents - = discount
2023-12-20 07:24:21 +01:00
2023-12-06 06:19:57 +01:00
charge_automatically = plan . charge_automatically
2024-01-12 16:54:34 +01:00
if customer . stripe_customer_id is not None :
stripe_customer = stripe_get_customer ( customer . stripe_customer_id )
stripe_email = stripe_customer . email
if charge_automatically :
payment_method = payment_method_string ( stripe_customer )
else :
2024-03-05 08:53:16 +01:00
payment_method = " Invoice "
2024-01-12 16:54:34 +01:00
elif settings . DEVELOPMENT : # nocoverage
# Allow access to billing page in development environment without a stripe_customer_id.
payment_method = " Payment method not populated "
stripe_email = " not_populated@zulip.com "
else : # nocoverage
raise BillingError ( f " stripe_customer_id is None for { customer } " )
2023-12-06 06:14:32 +01:00
2023-12-06 06:19:57 +01:00
remote_server_legacy_plan_end_date = self . get_formatted_remote_server_legacy_plan_end_date (
customer , status = CustomerPlan . SWITCH_PLAN_TIER_AT_PLAN_END
)
2023-12-09 09:00:34 +01:00
legacy_remote_server_next_plan_name = self . get_legacy_remote_server_next_plan_name ( customer )
2023-12-06 06:19:57 +01:00
context = {
" plan_name " : plan . name ,
" has_active_plan " : True ,
" free_trial " : plan . is_free_trial ( ) ,
" downgrade_at_end_of_cycle " : downgrade_at_end_of_cycle ,
" downgrade_at_end_of_free_trial " : downgrade_at_end_of_free_trial ,
" automanage_licenses " : plan . automanage_licenses ,
" switch_to_annual_at_end_of_cycle " : switch_to_annual_at_end_of_cycle ,
" switch_to_monthly_at_end_of_cycle " : switch_to_monthly_at_end_of_cycle ,
" licenses " : licenses ,
" licenses_at_next_renewal " : licenses_at_next_renewal ,
" seat_count " : seat_count ,
" renewal_date " : renewal_date ,
2024-02-28 09:26:25 +01:00
" renewal_amount " : cents_to_dollar_string ( renewal_cents ) if renewal_cents != 0 else None ,
2023-12-06 06:19:57 +01:00
" payment_method " : payment_method ,
" charge_automatically " : charge_automatically ,
2024-01-12 16:54:34 +01:00
" stripe_email " : stripe_email ,
2023-12-06 06:19:57 +01:00
" CustomerPlan " : CustomerPlan ,
" billing_frequency " : billing_frequency ,
2024-01-17 13:55:25 +01:00
" fixed_price_plan " : plan . fixed_price is not None ,
2023-12-06 06:19:57 +01:00
" price_per_license " : price_per_license ,
" is_sponsorship_pending " : customer . sponsorship_pending ,
2023-12-08 08:25:05 +01:00
" sponsorship_plan_name " : self . get_sponsorship_plan_name (
customer , is_self_hosted_billing
) ,
2024-05-06 06:12:15 +02:00
" discount_percent " : plan . discount ,
2023-12-08 08:25:05 +01:00
" is_self_hosted_billing " : is_self_hosted_billing ,
2023-12-06 06:19:57 +01:00
" is_server_on_legacy_plan " : remote_server_legacy_plan_end_date is not None ,
" remote_server_legacy_plan_end_date " : remote_server_legacy_plan_end_date ,
2023-12-09 09:00:34 +01:00
" legacy_remote_server_next_plan_name " : legacy_remote_server_next_plan_name ,
2023-12-09 08:16:53 +01:00
" using_min_licenses_for_plan " : using_min_licenses_for_plan ,
2023-12-18 12:16:29 +01:00
" min_licenses_for_plan " : min_licenses_for_plan ,
2023-12-20 07:24:21 +01:00
" pre_discount_renewal_cents " : cents_to_dollar_string ( pre_discount_renewal_cents ) ,
" flat_discount " : format_money ( customer . flat_discount ) ,
" discounted_months_left " : customer . flat_discounted_months ,
2024-04-10 10:31:15 +02:00
" has_paid_invoice_for_free_trial " : has_paid_invoice_for_free_trial ,
" free_trial_next_renewal_date_after_invoice_paid " : free_trial_next_renewal_date_after_invoice_paid ,
2023-12-06 06:19:57 +01:00
}
2023-11-16 16:14:43 +01:00
return context
2024-07-12 02:30:17 +02:00
def get_billing_page_context ( self ) - > dict [ str , Any ] :
2023-12-06 07:09:33 +01:00
now = timezone_now ( )
customer = self . get_customer ( )
assert customer is not None
plan = get_current_plan_by_customer ( customer )
assert plan is not None
new_plan , last_ledger_entry = self . make_end_of_cycle_updates_if_needed ( plan , now )
2024-03-19 13:33:13 +01:00
if last_ledger_entry is None :
return { " current_plan_downgraded " : True }
2023-12-06 07:09:33 +01:00
plan = new_plan if new_plan is not None else plan
context = self . get_billing_context_from_plan ( customer , plan , last_ledger_entry , now )
2023-12-06 08:21:13 +01:00
next_plan = self . get_next_plan ( plan )
2023-12-19 12:24:15 +01:00
if next_plan is not None :
2023-12-06 08:21:13 +01:00
next_plan_context = self . get_billing_context_from_plan (
customer , next_plan , last_ledger_entry , now
)
# Settings we want to display from the next plan instead of the current one.
# HACK: Our billing page is not designed to handle two plans, so while this is hacky,
# it's the easiest way to get the UI we want without making things too complicated for us.
keys = [
" renewal_amount " ,
2023-12-12 18:45:23 +01:00
" payment_method " ,
2023-12-06 08:21:13 +01:00
" charge_automatically " ,
" billing_frequency " ,
2024-01-17 13:55:25 +01:00
" fixed_price_plan " ,
2023-12-06 08:21:13 +01:00
" price_per_license " ,
" discount_percent " ,
2023-12-09 08:16:53 +01:00
" using_min_licenses_for_plan " ,
2023-12-20 07:24:21 +01:00
" min_licenses_for_plan " ,
" pre_discount_renewal_cents " ,
2023-12-06 08:21:13 +01:00
]
for key in keys :
context [ key ] = next_plan_context [ key ]
2023-12-06 07:09:33 +01:00
return context
2024-07-12 02:30:23 +02:00
def get_flat_discount_info ( self , customer : Customer | None = None ) - > tuple [ int , int ] :
2023-12-20 07:24:21 +01:00
is_self_hosted_billing = not isinstance ( self , RealmBillingSession )
flat_discount = 0
flat_discounted_months = 0
if is_self_hosted_billing and ( customer is None or customer . flat_discounted_months > 0 ) :
if customer is None :
temp_customer = Customer ( )
flat_discount = temp_customer . flat_discount
flat_discounted_months = 12
else :
flat_discount = customer . flat_discount
flat_discounted_months = customer . flat_discounted_months
assert isinstance ( flat_discount , int )
assert isinstance ( flat_discounted_months , int )
return flat_discount , flat_discounted_months
2023-11-20 20:32:29 +01:00
def get_initial_upgrade_context (
2023-11-20 08:40:09 +01:00
self , initial_upgrade_request : InitialUpgradeRequest
2024-07-12 02:30:23 +02:00
) - > tuple [ str | None , UpgradePageContext | None ] :
2023-11-20 08:40:09 +01:00
customer = self . get_customer ( )
2023-12-12 06:46:41 +01:00
# Allow users to upgrade to business regardless of current sponsorship status.
2023-12-18 23:57:32 +01:00
if self . is_sponsored_or_pending ( customer ) and initial_upgrade_request . tier not in [
CustomerPlan . TIER_SELF_HOSTED_BASIC ,
CustomerPlan . TIER_SELF_HOSTED_BUSINESS ,
] :
2023-11-24 09:08:24 +01:00
return f " { self . billing_session_url } /sponsorship " , None
2023-11-20 08:40:09 +01:00
2023-12-04 14:11:35 +01:00
remote_server_legacy_plan_end_date = self . get_formatted_remote_server_legacy_plan_end_date (
customer
)
# Show upgrade page for remote servers on legacy plan.
if customer is not None and remote_server_legacy_plan_end_date is None :
customer_plan = get_current_plan_by_customer ( customer )
if customer_plan is not None :
return f " { self . billing_session_url } /billing " , None
2023-11-20 08:40:09 +01:00
exempt_from_license_number_check = (
customer is not None and customer . exempt_from_license_number_check
)
2023-11-24 07:29:06 +01:00
# Check if user was successful in adding a card and we are rendering the page again.
current_payment_method = None
if customer is not None and customer_has_credit_card_as_default_payment_method ( customer ) :
assert customer . stripe_customer_id is not None
stripe_customer = stripe_get_customer ( customer . stripe_customer_id )
2024-01-23 08:03:25 +01:00
current_payment_method = payment_method_string ( stripe_customer )
2023-11-24 07:29:06 +01:00
2023-12-09 08:16:53 +01:00
tier = initial_upgrade_request . tier
2024-01-10 14:59:49 +01:00
2024-01-17 13:55:25 +01:00
fixed_price = None
2024-02-02 13:04:41 +01:00
pay_by_invoice_payments_page = None
2024-03-04 00:44:59 +01:00
scheduled_upgrade_invoice_amount_due = None
2024-04-10 10:31:15 +02:00
is_free_trial_invoice_expired_notice = False
free_trial_invoice_expired_notice_page_plan_name = None
2024-01-17 13:55:25 +01:00
if customer is not None :
fixed_price_plan_offer = get_configured_fixed_price_plan_offer ( customer , tier )
if fixed_price_plan_offer :
assert fixed_price_plan_offer . fixed_price is not None
fixed_price = fixed_price_plan_offer . fixed_price
2024-02-02 13:04:41 +01:00
if fixed_price_plan_offer . sent_invoice_id is not None :
invoice = stripe . Invoice . retrieve ( fixed_price_plan_offer . sent_invoice_id )
pay_by_invoice_payments_page = invoice . hosted_invoice_url
2024-03-04 00:44:59 +01:00
else :
# NOTE: Only use `last_send_invoice` to display invoice due information and not to verify payment.
2024-03-15 08:41:50 +01:00
# Since `last_send_invoice` can vary from invoice for upgrade, additional license, support contract etc.
2024-03-04 00:44:59 +01:00
last_send_invoice = (
Invoice . objects . filter ( customer = customer , status = Invoice . SENT )
. order_by ( " id " )
. last ( )
)
if last_send_invoice is not None :
invoice = stripe . Invoice . retrieve ( last_send_invoice . stripe_invoice_id )
if invoice is not None :
scheduled_upgrade_invoice_amount_due = format_money ( invoice . amount_due )
pay_by_invoice_payments_page = f " { self . billing_base_url } /invoices "
2024-02-02 13:04:41 +01:00
2024-04-10 10:31:15 +02:00
if (
last_send_invoice . plan is not None
and last_send_invoice . is_created_for_free_trial_upgrade
) :
# Automatic payment invoice would have been marked void already.
assert not last_send_invoice . plan . charge_automatically
is_free_trial_invoice_expired_notice = True
free_trial_invoice_expired_notice_page_plan_name = (
last_send_invoice . plan . name
)
2024-05-06 06:12:15 +02:00
annual_price , percent_off_annual_price = get_price_per_license_and_discount (
tier , CustomerPlan . BILLING_SCHEDULE_ANNUAL , customer
)
monthly_price , percent_off_monthly_price = get_price_per_license_and_discount (
tier , CustomerPlan . BILLING_SCHEDULE_MONTHLY , customer
)
2024-01-10 14:59:49 +01:00
2023-11-24 07:29:06 +01:00
customer_specific_context = self . get_upgrade_page_session_type_specific_context ( )
2023-12-09 08:16:53 +01:00
min_licenses_for_plan = self . min_licenses_for_plan ( tier )
2024-03-04 00:44:59 +01:00
setup_payment_by_invoice = initial_upgrade_request . billing_modality == " send_invoice "
# Regardless of value passed, invoice payments always have manual license management.
if setup_payment_by_invoice :
initial_upgrade_request . manual_license_management = True
2023-11-20 08:40:09 +01:00
seat_count = self . current_count_for_billed_licenses ( )
2023-12-09 08:16:53 +01:00
using_min_licenses_for_plan = min_licenses_for_plan > seat_count
if using_min_licenses_for_plan :
seat_count = min_licenses_for_plan
2023-11-20 08:40:09 +01:00
signed_seat_count , salt = sign_string ( str ( seat_count ) )
2023-11-25 15:18:56 +01:00
2023-12-04 14:11:35 +01:00
free_trial_days = None
2023-11-25 15:18:56 +01:00
free_trial_end_date = None
2023-12-04 14:11:35 +01:00
# Don't show free trial for remote servers on legacy plan.
2023-12-12 07:02:08 +01:00
is_self_hosted_billing = not isinstance ( self , RealmBillingSession )
2024-01-31 19:20:52 +01:00
if fixed_price is None and remote_server_legacy_plan_end_date is None :
2024-01-07 05:58:39 +01:00
free_trial_days = get_free_trial_days ( is_self_hosted_billing , tier )
2024-04-08 11:58:02 +02:00
if self . customer_plan_exists ( ) :
# Free trial is not available for existing customers.
2024-02-22 05:30:41 +01:00
free_trial_days = None
2023-12-04 14:11:35 +01:00
if free_trial_days is not None :
_ , _ , free_trial_end , _ = compute_plan_parameters (
2023-12-10 05:13:00 +01:00
tier ,
CustomerPlan . BILLING_SCHEDULE_ANNUAL ,
None ,
True ,
is_self_hosted_billing = is_self_hosted_billing ,
2023-12-04 14:11:35 +01:00
)
free_trial_end_date = (
f " { free_trial_end : %B } { free_trial_end . day } , { free_trial_end . year } "
)
2023-11-25 15:18:56 +01:00
2023-12-20 07:24:21 +01:00
flat_discount , flat_discounted_months = self . get_flat_discount_info ( customer )
2023-11-24 07:29:06 +01:00
context : UpgradePageContext = {
" customer_name " : customer_specific_context [ " customer_name " ] ,
" email " : customer_specific_context [ " email " ] ,
2023-11-20 08:40:09 +01:00
" exempt_from_license_number_check " : exempt_from_license_number_check ,
2023-11-25 15:18:56 +01:00
" free_trial_end_date " : free_trial_end_date ,
2023-11-24 07:29:06 +01:00
" is_demo_organization " : customer_specific_context [ " is_demo_organization " ] ,
2023-12-04 14:11:35 +01:00
" remote_server_legacy_plan_end_date " : remote_server_legacy_plan_end_date ,
2023-11-24 06:26:20 +01:00
" manual_license_management " : initial_upgrade_request . manual_license_management ,
2023-11-20 08:40:09 +01:00
" page_params " : {
2024-02-16 22:56:36 +01:00
" page_type " : " upgrade " ,
2024-05-06 06:12:15 +02:00
" annual_price " : annual_price ,
2023-11-24 07:29:06 +01:00
" demo_organization_scheduled_deletion_date " : customer_specific_context [
" demo_organization_scheduled_deletion_date "
] ,
2024-05-06 06:12:15 +02:00
" monthly_price " : monthly_price ,
2023-11-24 06:26:20 +01:00
" seat_count " : seat_count ,
2023-12-01 04:18:58 +01:00
" billing_base_url " : self . billing_base_url ,
2023-12-18 13:02:36 +01:00
" tier " : tier ,
2023-12-20 07:24:21 +01:00
" flat_discount " : flat_discount ,
" flat_discounted_months " : flat_discounted_months ,
2024-01-17 13:55:25 +01:00
" fixed_price " : fixed_price ,
2024-03-04 00:44:59 +01:00
" setup_payment_by_invoice " : setup_payment_by_invoice ,
" free_trial_days " : free_trial_days ,
2024-05-06 06:12:15 +02:00
" percent_off_annual_price " : percent_off_annual_price ,
" percent_off_monthly_price " : percent_off_monthly_price ,
2023-11-20 08:40:09 +01:00
} ,
2023-12-09 08:16:53 +01:00
" using_min_licenses_for_plan " : using_min_licenses_for_plan ,
2023-12-18 12:16:29 +01:00
" min_licenses_for_plan " : min_licenses_for_plan ,
2023-11-24 07:29:06 +01:00
" payment_method " : current_payment_method ,
2023-12-01 12:47:09 +01:00
" plan " : CustomerPlan . name_from_tier ( tier ) ,
2024-01-17 13:55:25 +01:00
" fixed_price_plan " : fixed_price is not None ,
2024-02-02 13:04:41 +01:00
" pay_by_invoice_payments_page " : pay_by_invoice_payments_page ,
2023-11-24 06:26:20 +01:00
" salt " : salt ,
" seat_count " : seat_count ,
" signed_seat_count " : signed_seat_count ,
2023-12-06 14:17:13 +01:00
" success_message " : initial_upgrade_request . success_message ,
2023-12-12 07:02:08 +01:00
" is_sponsorship_pending " : customer is not None and customer . sponsorship_pending ,
" sponsorship_plan_name " : self . get_sponsorship_plan_name (
customer , is_self_hosted_billing
) ,
2024-03-04 00:44:59 +01:00
" scheduled_upgrade_invoice_amount_due " : scheduled_upgrade_invoice_amount_due ,
2024-04-10 10:31:15 +02:00
" is_free_trial_invoice_expired_notice " : is_free_trial_invoice_expired_notice ,
" free_trial_invoice_expired_notice_page_plan_name " : free_trial_invoice_expired_notice_page_plan_name ,
2023-11-20 08:40:09 +01:00
}
2023-11-18 11:29:04 +01:00
2023-11-20 08:40:09 +01:00
return None , context
2024-01-06 07:52:20 +01:00
def min_licenses_for_flat_discount_to_self_hosted_basic_plan (
2024-04-10 10:31:15 +02:00
self ,
2024-07-12 02:30:23 +02:00
customer : Customer | None ,
2024-04-10 10:31:15 +02:00
is_plan_free_trial_with_invoice_payment : bool = False ,
2024-01-06 07:52:20 +01:00
) - > int :
# Since monthly and annual TIER_SELF_HOSTED_BASIC plans have same per user price we only need to do this calculation once.
# If we decided to apply this for other tiers, then we will have to do this calculation based on billing schedule selected by the user.
price_per_license = get_price_per_license (
CustomerPlan . TIER_SELF_HOSTED_BASIC , CustomerPlan . BILLING_SCHEDULE_MONTHLY
)
2024-04-10 10:31:15 +02:00
if customer is None or is_plan_free_trial_with_invoice_payment :
2024-01-06 07:52:20 +01:00
return (
Customer . _meta . get_field ( " flat_discount " ) . get_default ( ) / / price_per_license
) + 1
elif customer . flat_discounted_months > 0 :
return ( customer . flat_discount / / price_per_license ) + 1
# If flat discount is not applied.
return 1
2024-04-10 10:31:15 +02:00
def min_licenses_for_plan (
self , tier : int , is_plan_free_trial_with_invoice_payment : bool = False
) - > int :
2023-12-14 19:55:38 +01:00
customer = self . get_customer ( )
if customer is not None and customer . minimum_licenses :
2024-05-06 06:12:15 +02:00
assert customer . monthly_discounted_price or customer . annual_discounted_price
2023-12-14 19:55:38 +01:00
return customer . minimum_licenses
2023-12-18 23:57:32 +01:00
if tier == CustomerPlan . TIER_SELF_HOSTED_BASIC :
2024-04-10 10:31:15 +02:00
return min (
self . min_licenses_for_flat_discount_to_self_hosted_basic_plan (
customer ,
is_plan_free_trial_with_invoice_payment ,
) ,
10 ,
)
2023-12-18 23:57:32 +01:00
if tier == CustomerPlan . TIER_SELF_HOSTED_BUSINESS :
return 25
2023-12-09 08:16:53 +01:00
return 1
2024-07-12 02:30:23 +02:00
def downgrade_at_the_end_of_billing_cycle ( self , plan : CustomerPlan | None = None ) - > None :
2023-11-22 12:44:02 +01:00
if plan is None : # nocoverage
# TODO: Add test coverage. Right now, this logic is used
# in production but mocked in tests.
customer = self . get_customer ( )
assert customer is not None
plan = get_current_plan_by_customer ( customer )
assert plan is not None
do_change_plan_status ( plan , CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE )
2023-12-01 17:48:41 +01:00
def void_all_open_invoices ( self ) - > int :
customer = self . get_customer ( )
if customer is None :
return 0
invoices = get_all_invoices_for_customer ( customer )
voided_invoices_count = 0
for invoice in invoices :
if invoice . status == " open " :
assert invoice . id is not None
stripe . Invoice . void_invoice ( invoice . id )
voided_invoices_count + = 1
return voided_invoices_count
2023-11-22 12:44:02 +01:00
# During realm deactivation we instantly downgrade the plan to Limited.
# Extra users added in the final month are not charged. Also used
# for the cancellation of Free Trial.
def downgrade_now_without_creating_additional_invoices (
self ,
2024-07-12 02:30:23 +02:00
plan : CustomerPlan | None = None ,
2023-12-24 15:56:33 +01:00
background_update : bool = False ,
2023-11-22 12:44:02 +01:00
) - > None :
if plan is None :
customer = self . get_customer ( )
if customer is None :
return
plan = get_current_plan_by_customer ( customer )
if plan is None :
return # nocoverage
2023-12-24 15:56:33 +01:00
self . process_downgrade ( plan , background_update = background_update )
2023-11-22 12:44:02 +01:00
plan . invoiced_through = LicenseLedger . objects . filter ( plan = plan ) . order_by ( " id " ) . last ( )
plan . next_invoice_date = next_invoice_date ( plan )
plan . save ( update_fields = [ " invoiced_through " , " next_invoice_date " ] )
def do_update_plan ( self , update_plan_request : UpdatePlanRequest ) - > None :
customer = self . get_customer ( )
assert customer is not None
plan = get_current_plan_by_customer ( customer )
assert plan is not None # for mypy
new_plan , last_ledger_entry = self . make_end_of_cycle_updates_if_needed ( plan , timezone_now ( ) )
if new_plan is not None :
raise JsonableError (
_ (
" Unable to update the plan. The plan has been expired and replaced with a new plan. "
)
)
if last_ledger_entry is None :
raise JsonableError ( _ ( " Unable to update the plan. The plan has ended. " ) )
status = update_plan_request . status
if status is not None :
if status == CustomerPlan . ACTIVE :
assert plan . status < CustomerPlan . LIVE_STATUS_THRESHOLD
2023-12-19 12:24:15 +01:00
with transaction . atomic ( ) :
2023-12-06 13:37:19 +01:00
# Switch to a different plan was cancelled. We end the next plan
# and set the current one as active.
if plan . status == CustomerPlan . SWITCH_PLAN_TIER_AT_PLAN_END :
next_plan = self . get_next_plan ( plan )
2024-01-31 14:10:45 +01:00
assert next_plan is not None
2023-12-06 13:37:19 +01:00
do_change_plan_status ( next_plan , CustomerPlan . ENDED )
do_change_plan_status ( plan , status )
2023-11-22 12:44:02 +01:00
elif status == CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE :
2023-11-25 11:40:13 +01:00
assert not plan . is_free_trial ( )
2023-11-22 12:44:02 +01:00
assert plan . status < CustomerPlan . LIVE_STATUS_THRESHOLD
self . downgrade_at_the_end_of_billing_cycle ( plan = plan )
elif status == CustomerPlan . SWITCH_TO_ANNUAL_AT_END_OF_CYCLE :
2023-11-30 07:55:53 +01:00
assert plan . billing_schedule == CustomerPlan . BILLING_SCHEDULE_MONTHLY
2023-11-22 12:44:02 +01:00
assert plan . status < CustomerPlan . LIVE_STATUS_THRESHOLD
# Customer needs to switch to an active plan first to avoid unexpected behavior.
assert plan . status != CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE
2023-11-25 11:40:13 +01:00
# Switching billing frequency for free trial should happen instantly.
assert not plan . is_free_trial ( )
2023-11-22 12:44:02 +01:00
assert plan . fixed_price is None
do_change_plan_status ( plan , status )
elif status == CustomerPlan . SWITCH_TO_MONTHLY_AT_END_OF_CYCLE :
2023-11-30 07:55:53 +01:00
assert plan . billing_schedule == CustomerPlan . BILLING_SCHEDULE_ANNUAL
2023-11-22 12:44:02 +01:00
assert plan . status < CustomerPlan . LIVE_STATUS_THRESHOLD
# Customer needs to switch to an active plan first to avoid unexpected behavior.
assert plan . status != CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE
2023-11-25 11:40:13 +01:00
# Switching billing frequency for free trial should happen instantly.
assert not plan . is_free_trial ( )
2023-11-22 12:44:02 +01:00
assert plan . fixed_price is None
do_change_plan_status ( plan , status )
elif status == CustomerPlan . ENDED :
2023-11-25 14:44:47 +01:00
# Not used right now on billing page but kept in case we need it.
2023-11-22 12:44:02 +01:00
assert plan . is_free_trial ( )
self . downgrade_now_without_creating_additional_invoices ( plan = plan )
2023-11-25 14:44:47 +01:00
elif status == CustomerPlan . DOWNGRADE_AT_END_OF_FREE_TRIAL :
assert plan . is_free_trial ( )
2024-04-10 10:31:15 +02:00
# For payment by invoice, we don't allow changing plan schedule and status.
assert plan . charge_automatically
2023-11-25 14:44:47 +01:00
do_change_plan_status ( plan , status )
elif status == CustomerPlan . FREE_TRIAL :
2024-04-10 10:31:15 +02:00
assert plan . charge_automatically
2023-11-26 15:41:28 +01:00
if update_plan_request . schedule is not None :
self . do_change_schedule_after_free_trial ( plan , update_plan_request . schedule )
else :
assert plan . status == CustomerPlan . DOWNGRADE_AT_END_OF_FREE_TRIAL
do_change_plan_status ( plan , status )
2023-11-22 12:44:02 +01:00
return
licenses = update_plan_request . licenses
if licenses is not None :
2023-12-08 02:45:11 +01:00
if plan . is_free_trial ( ) : # nocoverage
raise JsonableError (
_ ( " Cannot update licenses in the current billing period for free trial plan. " )
)
2023-11-22 12:44:02 +01:00
if plan . automanage_licenses :
raise JsonableError (
_ (
" Unable to update licenses manually. Your plan is on automatic license management. "
)
)
if last_ledger_entry . licenses == licenses :
raise JsonableError (
_ (
" Your plan is already on {licenses} licenses in the current billing period. "
) . format ( licenses = licenses )
)
if last_ledger_entry . licenses > licenses :
raise JsonableError (
_ ( " You cannot decrease the licenses in the current billing period. " )
)
validate_licenses (
plan . charge_automatically ,
licenses ,
self . current_count_for_billed_licenses ( ) ,
plan . customer . exempt_from_license_number_check ,
2023-12-09 08:16:53 +01:00
self . min_licenses_for_plan ( plan . tier ) ,
2023-11-22 12:44:02 +01:00
)
2023-12-05 19:47:32 +01:00
self . update_license_ledger_for_manual_plan ( plan , timezone_now ( ) , licenses = licenses )
2023-11-22 12:44:02 +01:00
return
licenses_at_next_renewal = update_plan_request . licenses_at_next_renewal
if licenses_at_next_renewal is not None :
if plan . automanage_licenses :
raise JsonableError (
_ (
" Unable to update licenses manually. Your plan is on automatic license management. "
)
)
2023-12-08 02:45:11 +01:00
if plan . status in (
CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE ,
CustomerPlan . DOWNGRADE_AT_END_OF_FREE_TRIAL ,
) : # nocoverage
raise JsonableError (
_ (
" Cannot change the licenses for next billing cycle for a plan that is being downgraded. "
)
)
2023-11-22 12:44:02 +01:00
if last_ledger_entry . licenses_at_next_renewal == licenses_at_next_renewal :
raise JsonableError (
_ (
" Your plan is already scheduled to renew with {licenses_at_next_renewal} licenses. "
) . format ( licenses_at_next_renewal = licenses_at_next_renewal )
)
2024-04-10 10:31:15 +02:00
is_plan_free_trial_with_invoice_payment = (
plan . is_free_trial ( ) and not plan . charge_automatically
)
2023-11-22 12:44:02 +01:00
validate_licenses (
plan . charge_automatically ,
licenses_at_next_renewal ,
self . current_count_for_billed_licenses ( ) ,
plan . customer . exempt_from_license_number_check ,
2024-04-10 10:31:15 +02:00
self . min_licenses_for_plan ( plan . tier , is_plan_free_trial_with_invoice_payment ) ,
2023-11-22 12:44:02 +01:00
)
2024-04-10 10:31:15 +02:00
# User is trying to change licenses while in free trial.
if is_plan_free_trial_with_invoice_payment : # nocoverage
invoice = Invoice . objects . filter ( plan = plan ) . order_by ( " -id " ) . first ( )
assert invoice is not None
# Don't allow customer to reduce licenses for next billing cycle if they have paid invoice.
if invoice . status == Invoice . PAID :
assert last_ledger_entry . licenses_at_next_renewal is not None
if last_ledger_entry . licenses_at_next_renewal > licenses_at_next_renewal :
raise JsonableError (
_ (
" You’ ve already purchased {licenses_at_next_renewal} licenses for the next billing period. "
) . format (
licenses_at_next_renewal = last_ledger_entry . licenses_at_next_renewal
)
)
else :
# If customer has paid already, we will send them an invoice for additional
# licenses at the end of free trial.
self . update_license_ledger_for_manual_plan (
plan , timezone_now ( ) , licenses_at_next_renewal = licenses_at_next_renewal
)
else :
# Discard the old invoice and create a new one with updated licenses.
self . update_free_trial_invoice_with_licenses (
plan , timezone_now ( ) , licenses_at_next_renewal
)
else :
self . update_license_ledger_for_manual_plan (
plan , timezone_now ( ) , licenses_at_next_renewal = licenses_at_next_renewal
)
2023-11-22 12:44:02 +01:00
return
raise JsonableError ( _ ( " Nothing to change. " ) )
2023-11-23 07:29:03 +01:00
def switch_plan_tier ( self , current_plan : CustomerPlan , new_plan_tier : int ) - > None :
assert current_plan . status == CustomerPlan . SWITCH_PLAN_TIER_NOW
assert current_plan . next_invoice_date is not None
next_billing_cycle = current_plan . next_invoice_date
current_plan . end_date = next_billing_cycle
current_plan . status = CustomerPlan . ENDED
current_plan . save ( update_fields = [ " status " , " end_date " ] )
2024-05-06 06:12:15 +02:00
new_price_per_license , discount_for_new_plan_tier = get_price_per_license_and_discount (
new_plan_tier , current_plan . billing_schedule , current_plan . customer
2023-11-23 07:29:03 +01:00
)
new_plan_billing_cycle_anchor = current_plan . end_date . replace ( microsecond = 0 )
new_plan = CustomerPlan . objects . create (
customer = current_plan . customer ,
status = CustomerPlan . ACTIVE ,
automanage_licenses = current_plan . automanage_licenses ,
charge_automatically = current_plan . charge_automatically ,
price_per_license = new_price_per_license ,
2024-01-10 14:59:49 +01:00
discount = discount_for_new_plan_tier ,
2023-11-23 07:29:03 +01:00
billing_schedule = current_plan . billing_schedule ,
tier = new_plan_tier ,
billing_cycle_anchor = new_plan_billing_cycle_anchor ,
2023-11-30 08:07:12 +01:00
invoicing_status = CustomerPlan . INVOICING_STATUS_INITIAL_INVOICE_TO_BE_SENT ,
2023-11-23 07:29:03 +01:00
next_invoice_date = new_plan_billing_cycle_anchor ,
)
current_plan_last_ledger = (
LicenseLedger . objects . filter ( plan = current_plan ) . order_by ( " id " ) . last ( )
)
assert current_plan_last_ledger is not None
licenses_for_new_plan = current_plan_last_ledger . licenses_at_next_renewal
assert licenses_for_new_plan is not None
LicenseLedger . objects . create (
plan = new_plan ,
is_renewal = True ,
event_time = new_plan_billing_cycle_anchor ,
licenses = licenses_for_new_plan ,
licenses_at_next_renewal = licenses_for_new_plan ,
)
2023-11-30 14:49:10 +01:00
def invoice_plan ( self , plan : CustomerPlan , event_time : datetime ) - > None :
if plan . invoicing_status == CustomerPlan . INVOICING_STATUS_STARTED :
raise NotImplementedError (
" Plan with invoicing_status==STARTED needs manual resolution. "
)
2024-01-22 14:20:49 +01:00
if (
plan . tier != CustomerPlan . TIER_SELF_HOSTED_LEGACY
and not plan . customer . stripe_customer_id
) :
2023-11-30 14:49:10 +01:00
raise BillingError (
f " Customer has a paid plan without a Stripe customer ID: { plan . customer !s} "
)
# Updating a CustomerPlan with a status to switch the plan tier,
# is done via switch_plan_tier, so we do not need to make end of
# cycle updates for that case.
if plan . status is not CustomerPlan . SWITCH_PLAN_TIER_NOW :
self . make_end_of_cycle_updates_if_needed ( plan , event_time )
2024-02-20 06:06:09 +01:00
# The primary way to not create an invoice for a plan is to not have
2024-03-12 19:48:16 +01:00
# any new ledger entry. The 'plan.is_a_paid_plan()' check adds an extra
2024-02-20 06:06:09 +01:00
# layer of defense to avoid creating any invoices for customers not on
# paid plan. It saves a DB query too.
2024-03-12 19:48:16 +01:00
if plan . is_a_paid_plan ( ) :
2024-04-30 19:43:16 +02:00
assert plan . customer . stripe_customer_id is not None
2024-02-20 06:06:09 +01:00
if plan . invoicing_status == CustomerPlan . INVOICING_STATUS_INITIAL_INVOICE_TO_BE_SENT :
invoiced_through_id = - 1
licenses_base = None
else :
assert plan . invoiced_through is not None
licenses_base = plan . invoiced_through . licenses
invoiced_through_id = plan . invoiced_through . id
invoice_item_created = False
2024-04-30 19:23:16 +02:00
invoice_period : stripe . InvoiceItem . CreateParamsPeriod | None = None
2024-02-20 06:06:09 +01:00
for ledger_entry in LicenseLedger . objects . filter (
plan = plan , id__gt = invoiced_through_id , event_time__lte = event_time
) . order_by ( " id " ) :
price_args : PriceArgs = { }
if ledger_entry . is_renewal :
if plan . fixed_price is not None :
amount_due = get_amount_due_fixed_price_plan (
plan . fixed_price , plan . billing_schedule
)
price_args = { " amount " : amount_due }
else :
assert plan . price_per_license is not None # needed for mypy
price_args = {
" unit_amount " : plan . price_per_license ,
" quantity " : ledger_entry . licenses ,
}
description = f " { plan . name } - renewal "
2024-03-07 12:22:57 +01:00
elif (
plan . fixed_price is None
and licenses_base is not None
and ledger_entry . licenses != licenses_base
) :
2024-03-07 12:37:45 +01:00
assert plan . price_per_license is not None
2024-02-20 06:06:09 +01:00
last_ledger_entry_renewal = (
LicenseLedger . objects . filter (
plan = plan , is_renewal = True , event_time__lte = ledger_entry . event_time
)
. order_by ( " -id " )
. first ( )
)
assert last_ledger_entry_renewal is not None
last_renewal = last_ledger_entry_renewal . event_time
billing_period_end = start_of_next_billing_cycle ( plan , ledger_entry . event_time )
plan_renewal_or_end_date = get_plan_renewal_or_end_date (
plan , ledger_entry . event_time
)
2024-04-10 10:31:15 +02:00
unit_amount = plan . price_per_license
if not plan . is_free_trial ( ) :
proration_fraction = (
plan_renewal_or_end_date - ledger_entry . event_time
) / ( billing_period_end - last_renewal )
unit_amount = int ( plan . price_per_license * proration_fraction + 0.5 )
2023-11-30 14:49:10 +01:00
price_args = {
2024-04-10 10:31:15 +02:00
" unit_amount " : unit_amount ,
2024-02-20 06:06:09 +01:00
" quantity " : ledger_entry . licenses - licenses_base ,
2023-11-30 14:49:10 +01:00
}
2024-02-20 06:06:09 +01:00
description = " Additional license ( {} - {} ) " . format (
ledger_entry . event_time . strftime ( " % b %-d , % Y " ) ,
plan_renewal_or_end_date . strftime ( " % b %-d , % Y " ) ,
2023-11-30 14:49:10 +01:00
)
2024-02-20 06:06:09 +01:00
if price_args :
plan . invoiced_through = ledger_entry
plan . invoicing_status = CustomerPlan . INVOICING_STATUS_STARTED
plan . save ( update_fields = [ " invoicing_status " , " invoiced_through " ] )
2024-03-30 04:59:59 +01:00
invoice_period = {
" start " : datetime_to_timestamp ( ledger_entry . event_time ) ,
" end " : datetime_to_timestamp (
get_plan_renewal_or_end_date ( plan , ledger_entry . event_time )
) ,
}
2024-02-20 06:06:09 +01:00
stripe . InvoiceItem . create (
currency = " usd " ,
customer = plan . customer . stripe_customer_id ,
description = description ,
discountable = False ,
2024-03-30 04:59:59 +01:00
period = invoice_period ,
2024-02-20 06:06:09 +01:00
idempotency_key = get_idempotency_key ( ledger_entry ) ,
* * price_args ,
)
invoice_item_created = True
2023-11-30 14:49:10 +01:00
plan . invoiced_through = ledger_entry
2024-02-20 06:06:09 +01:00
plan . invoicing_status = CustomerPlan . INVOICING_STATUS_DONE
2023-11-30 14:49:10 +01:00
plan . save ( update_fields = [ " invoicing_status " , " invoiced_through " ] )
2024-02-20 06:06:09 +01:00
licenses_base = ledger_entry . licenses
if invoice_item_created :
2024-04-30 20:02:47 +02:00
assert invoice_period is not None
2024-02-20 06:06:09 +01:00
flat_discount , flat_discounted_months = self . get_flat_discount_info ( plan . customer )
if plan . fixed_price is None and flat_discounted_months > 0 :
num_months = (
12 if plan . billing_schedule == CustomerPlan . BILLING_SCHEDULE_ANNUAL else 1
)
flat_discounted_months = min ( flat_discounted_months , num_months )
discount = flat_discount * flat_discounted_months
plan . customer . flat_discounted_months - = flat_discounted_months
plan . customer . save ( update_fields = [ " flat_discounted_months " ] )
stripe . InvoiceItem . create (
currency = " usd " ,
customer = plan . customer . stripe_customer_id ,
description = f " $ { cents_to_dollar_string ( flat_discount ) } /month new customer discount " ,
# Negative value to apply discount.
amount = ( - 1 * discount ) ,
2024-03-30 04:59:59 +01:00
period = invoice_period ,
2024-02-20 06:06:09 +01:00
)
if plan . charge_automatically :
2024-06-10 20:35:19 +02:00
collection_method : Literal [ " charge_automatically " , " send_invoice " ] = (
2024-04-30 19:23:16 +02:00
" charge_automatically "
)
2024-02-20 06:06:09 +01:00
days_until_due = None
else :
collection_method = " send_invoice "
days_until_due = DEFAULT_INVOICE_DAYS_UNTIL_DUE
2024-04-30 19:37:55 +02:00
invoice_params = stripe . Invoice . CreateParams (
2024-02-20 06:06:09 +01:00
auto_advance = True ,
collection_method = collection_method ,
2024-01-09 13:47:15 +01:00
customer = plan . customer . stripe_customer_id ,
2024-02-20 06:06:09 +01:00
statement_descriptor = plan . name ,
2024-01-09 13:47:15 +01:00
)
2024-04-30 19:37:55 +02:00
if days_until_due is not None :
invoice_params [ " days_until_due " ] = days_until_due
stripe_invoice = stripe . Invoice . create ( * * invoice_params )
2024-02-20 06:06:09 +01:00
stripe . Invoice . finalize_invoice ( stripe_invoice )
2023-11-30 14:49:10 +01:00
plan . next_invoice_date = next_invoice_date ( plan )
2024-01-08 13:28:06 +01:00
plan . invoice_overdue_email_sent = False
plan . save ( update_fields = [ " next_invoice_date " , " invoice_overdue_email_sent " ] )
2023-11-30 14:49:10 +01:00
2023-12-01 19:45:11 +01:00
def do_change_plan_to_new_tier ( self , new_plan_tier : int ) - > str :
2023-11-23 07:29:03 +01:00
customer = self . get_customer ( )
assert customer is not None
current_plan = get_current_plan_by_customer ( customer )
if not current_plan or current_plan . status != CustomerPlan . ACTIVE :
raise BillingError ( " Organization does not have an active plan " )
if not current_plan . customer . stripe_customer_id :
raise BillingError ( " Organization missing Stripe customer. " )
2023-11-30 17:11:41 +01:00
type_of_tier_change = self . get_type_of_plan_tier_change ( current_plan . tier , new_plan_tier )
if type_of_tier_change == PlanTierChangeType . INVALID :
2023-11-23 07:29:03 +01:00
raise BillingError ( " Invalid change of customer plan tier. " )
2023-11-30 17:11:41 +01:00
if type_of_tier_change == PlanTierChangeType . UPGRADE :
plan_switch_time = timezone_now ( )
current_plan . status = CustomerPlan . SWITCH_PLAN_TIER_NOW
current_plan . next_invoice_date = plan_switch_time
current_plan . save ( update_fields = [ " status " , " next_invoice_date " ] )
2023-11-23 07:29:03 +01:00
2023-11-30 17:11:41 +01:00
self . do_change_plan_type ( tier = new_plan_tier )
2023-11-23 07:29:03 +01:00
2023-11-30 17:11:41 +01:00
amount_to_credit_for_early_termination = get_amount_to_credit_for_plan_tier_change (
current_plan , plan_switch_time
)
stripe . Customer . create_balance_transaction (
current_plan . customer . stripe_customer_id ,
amount = - 1 * amount_to_credit_for_early_termination ,
currency = " usd " ,
description = " Credit from early termination of active plan " ,
)
self . switch_plan_tier ( current_plan , new_plan_tier )
self . invoice_plan ( current_plan , plan_switch_time )
new_plan = get_current_plan_by_customer ( customer )
assert new_plan is not None # for mypy
self . invoice_plan ( new_plan , plan_switch_time )
2023-12-01 19:45:11 +01:00
return f " { self . billing_entity_display_name } upgraded to { new_plan . name } "
2023-11-23 07:29:03 +01:00
2023-11-30 17:11:41 +01:00
# TODO: Implement downgrade that is a change from and to a paid plan
# tier. This should keep the same billing cycle schedule and change
# the plan when it's next invoiced vs immediately. Note this will need
2023-12-04 14:03:24 +01:00
# new CustomerPlan.status value, e.g. SWITCH_PLAN_TIER_AT_PLAN_END.
2023-11-30 17:11:41 +01:00
assert type_of_tier_change == PlanTierChangeType . DOWNGRADE # nocoverage
2023-12-01 19:45:11 +01:00
return " " # nocoverage
2023-11-23 07:29:03 +01:00
2024-07-12 02:30:17 +02:00
def get_event_status ( self , event_status_request : EventStatusRequest ) - > dict [ str , Any ] :
2023-11-27 11:07:03 +01:00
customer = self . get_customer ( )
if customer is None :
raise JsonableError ( _ ( " No customer for this organization! " ) )
stripe_session_id = event_status_request . stripe_session_id
if stripe_session_id is not None :
try :
session = Session . objects . get (
stripe_session_id = stripe_session_id , customer = customer
)
except Session . DoesNotExist :
raise JsonableError ( _ ( " Session not found " ) )
if (
session . type == Session . CARD_UPDATE_FROM_BILLING_PAGE
and not self . has_billing_access ( )
) :
raise JsonableError ( _ ( " Must be a billing administrator or an organization owner " ) )
return { " session " : session . to_dict ( ) }
2024-02-10 07:47:32 +01:00
stripe_invoice_id = event_status_request . stripe_invoice_id
if stripe_invoice_id is not None :
stripe_invoice = Invoice . objects . filter (
stripe_invoice_id = stripe_invoice_id ,
2023-11-27 11:07:03 +01:00
customer = customer ,
) . last ( )
2024-02-10 07:47:32 +01:00
if stripe_invoice is None :
2023-11-27 11:07:03 +01:00
raise JsonableError ( _ ( " Payment intent not found " ) )
2024-02-10 07:47:32 +01:00
return { " stripe_invoice " : stripe_invoice . to_dict ( ) }
2023-11-27 11:07:03 +01:00
2024-02-10 07:47:32 +01:00
raise JsonableError ( _ ( " Pass stripe_session_id or stripe_invoice_id " ) )
2023-11-27 11:07:03 +01:00
2024-07-12 02:30:23 +02:00
def get_sponsorship_plan_name ( self , customer : Customer | None , is_remotely_hosted : bool ) - > str :
2023-12-08 08:25:05 +01:00
if customer is not None and customer . sponsorship_pending :
# For sponsorship pending requests, we also show the type of sponsorship requested.
# In other cases, we just show the plan user is currently on.
sponsorship_request = (
ZulipSponsorshipRequest . objects . filter ( customer = customer ) . order_by ( " -id " ) . first ( )
)
# It's possible that we marked `customer.sponsorship_pending` via support page
# without user submitting a sponsorship request.
if sponsorship_request is not None and sponsorship_request . requested_plan not in (
None ,
SponsoredPlanTypes . UNSPECIFIED . value ,
) : # nocoverage
return sponsorship_request . requested_plan
2023-12-05 07:41:34 +01:00
2023-12-12 06:40:25 +01:00
# Default name for sponsorship plan.
2023-12-05 07:41:34 +01:00
sponsored_plan_name = CustomerPlan . name_from_tier ( CustomerPlan . TIER_CLOUD_STANDARD )
if is_remotely_hosted :
sponsored_plan_name = CustomerPlan . name_from_tier (
CustomerPlan . TIER_SELF_HOSTED_COMMUNITY
)
2023-12-08 08:25:05 +01:00
return sponsored_plan_name
2024-07-12 02:30:23 +02:00
def get_sponsorship_request_context ( self ) - > dict [ str , Any ] | None :
2023-12-08 08:25:05 +01:00
customer = self . get_customer ( )
is_remotely_hosted = isinstance (
2024-07-12 02:30:27 +02:00
self , RemoteRealmBillingSession | RemoteServerBillingSession
2023-12-08 08:25:05 +01:00
)
2023-12-05 07:41:34 +01:00
plan_name = " Zulip Cloud Free "
if is_remotely_hosted :
2024-02-13 05:58:20 +01:00
plan_name = " Free "
2023-12-05 07:41:34 +01:00
2024-07-12 02:30:17 +02:00
context : dict [ str , Any ] = {
2023-12-05 07:41:34 +01:00
" billing_base_url " : self . billing_base_url ,
" is_remotely_hosted " : is_remotely_hosted ,
2023-12-08 08:25:05 +01:00
" sponsorship_plan_name " : self . get_sponsorship_plan_name ( customer , is_remotely_hosted ) ,
2023-12-05 07:41:34 +01:00
" plan_name " : plan_name ,
2024-02-01 05:07:01 +01:00
" org_name " : self . org_name ( ) ,
2023-12-05 07:41:34 +01:00
}
2023-11-27 13:08:43 +01:00
if customer is not None and customer . sponsorship_pending :
if self . on_paid_plan ( ) :
return None
context [ " is_sponsorship_pending " ] = True
if self . is_sponsored ( ) :
context [ " is_sponsored " ] = True
if customer is not None :
plan = get_current_plan_by_customer ( customer )
if plan is not None :
context [ " plan_name " ] = plan . name
context [ " free_trial " ] = plan . is_free_trial ( )
2023-12-14 08:38:54 +01:00
context [ " is_server_on_legacy_plan " ] = (
plan . tier == CustomerPlan . TIER_SELF_HOSTED_LEGACY
)
2023-11-27 13:08:43 +01:00
self . add_sponsorship_info_to_context ( context )
return context
2023-11-30 01:48:46 +01:00
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 " ] ,
2023-12-08 08:25:05 +01:00
requested_plan = form . cleaned_data [ " requested_plan " ] ,
2023-11-30 01:48:46 +01:00
)
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 " ] ,
2023-12-06 13:20:22 +01:00
" billing_entity " : self . billing_entity_display_name ,
2023-11-30 01:48:46 +01:00
" 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 ,
2023-12-08 08:25:05 +01:00
" requested_plan " : sponsorship_request . requested_plan ,
2023-12-17 13:32:48 +01:00
" is_cloud_organization " : isinstance ( self , RealmBillingSession ) ,
2023-11-30 01:48:46 +01:00
}
send_email (
" zerver/emails/sponsorship_request " ,
2023-12-14 13:45:31 +01:00
to_emails = [ BILLING_SUPPORT_EMAIL ] ,
2023-11-30 01:48:46 +01:00
# 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 ,
)
2023-11-30 21:11:54 +01:00
def process_support_view_request ( self , support_request : SupportViewRequest ) - > str :
support_type = support_request [ " support_type " ]
success_message = " "
if support_type == SupportType . approve_sponsorship :
success_message = self . approve_sponsorship ( )
elif support_type == SupportType . update_sponsorship_status :
assert support_request [ " sponsorship_status " ] is not None
sponsorship_status = support_request [ " sponsorship_status " ]
success_message = self . update_customer_sponsorship_status ( sponsorship_status )
elif support_type == SupportType . attach_discount :
2024-05-06 06:12:15 +02:00
monthly_discounted_price = support_request [ " monthly_discounted_price " ]
annual_discounted_price = support_request [ " annual_discounted_price " ]
assert monthly_discounted_price is not None
assert annual_discounted_price is not None
success_message = self . attach_discount_to_customer (
monthly_discounted_price , annual_discounted_price
)
2023-12-14 19:55:38 +01:00
elif support_type == SupportType . update_minimum_licenses :
assert support_request [ " minimum_licenses " ] is not None
new_minimum_license_count = support_request [ " minimum_licenses " ]
success_message = self . update_customer_minimum_licenses ( new_minimum_license_count )
2024-01-10 17:20:08 +01:00
elif support_type == SupportType . update_required_plan_tier :
required_plan_tier = support_request . get ( " required_plan_tier " )
assert required_plan_tier is not None
success_message = self . set_required_plan_tier ( required_plan_tier )
2024-01-17 13:55:25 +01:00
elif support_type == SupportType . configure_fixed_price_plan :
assert support_request [ " fixed_price " ] is not None
new_fixed_price = support_request [ " fixed_price " ]
2024-02-02 13:04:41 +01:00
sent_invoice_id = support_request [ " sent_invoice_id " ]
success_message = self . configure_fixed_price_plan ( new_fixed_price , sent_invoice_id )
2024-08-15 18:29:51 +02:00
elif support_type == SupportType . configure_temporary_courtesy_plan :
assert support_request [ " plan_end_date " ] is not None
temporary_plan_end_date = support_request [ " plan_end_date " ]
success_message = self . configure_temporary_courtesy_plan ( temporary_plan_end_date )
2023-12-01 12:23:31 +01:00
elif support_type == SupportType . update_billing_modality :
assert support_request [ " billing_modality " ] is not None
assert support_request [ " billing_modality " ] in VALID_BILLING_MODALITY_VALUES
charge_automatically = support_request [ " billing_modality " ] == " charge_automatically "
success_message = self . update_billing_modality_of_current_plan ( charge_automatically )
2024-01-08 20:34:16 +01:00
elif support_type == SupportType . update_plan_end_date :
assert support_request [ " plan_end_date " ] is not None
new_plan_end_date = support_request [ " plan_end_date " ]
success_message = self . update_end_date_of_current_plan ( new_plan_end_date )
2023-12-01 19:45:11 +01:00
elif support_type == SupportType . modify_plan :
assert support_request [ " plan_modification " ] is not None
plan_modification = support_request [ " plan_modification " ]
if plan_modification == " downgrade_at_billing_cycle_end " :
self . downgrade_at_the_end_of_billing_cycle ( )
success_message = f " { self . billing_entity_display_name } marked for downgrade at the end of billing cycle "
elif plan_modification == " downgrade_now_without_additional_licenses " :
self . downgrade_now_without_creating_additional_invoices ( )
success_message = f " { self . billing_entity_display_name } downgraded without creating additional invoices "
elif plan_modification == " downgrade_now_void_open_invoices " :
self . downgrade_now_without_creating_additional_invoices ( )
voided_invoices_count = self . void_all_open_invoices ( )
success_message = f " { self . billing_entity_display_name } downgraded and voided { voided_invoices_count } open invoices "
else :
assert plan_modification == " upgrade_plan_tier "
assert support_request [ " new_plan_tier " ] is not None
new_plan_tier = support_request [ " new_plan_tier " ]
success_message = self . do_change_plan_to_new_tier ( new_plan_tier )
2024-02-13 13:23:07 +01:00
elif support_type == SupportType . delete_fixed_price_next_plan :
customer = self . get_customer ( )
assert customer is not None
fixed_price_offer = CustomerPlanOffer . objects . filter (
customer = customer , status = CustomerPlanOffer . CONFIGURED
) . first ( )
assert fixed_price_offer is not None
fixed_price_offer . delete ( )
success_message = " Fixed price offer deleted "
2023-11-30 21:11:54 +01:00
return success_message
2024-04-10 10:31:15 +02:00
def update_free_trial_invoice_with_licenses (
self ,
plan : CustomerPlan ,
event_time : datetime ,
licenses : int ,
) - > None : # nocoverage
assert (
self . get_billable_licenses_for_customer ( plan . customer , plan . tier , licenses ) < = licenses
)
last_sent_invoice = Invoice . objects . filter ( plan = plan ) . order_by ( " -id " ) . first ( )
assert last_sent_invoice is not None
assert last_sent_invoice . status == Invoice . SENT
assert plan . automanage_licenses is False
assert plan . charge_automatically is False
assert plan . fixed_price is None
assert plan . is_free_trial ( )
# Create a new renewal invoice with updated licenses so that this becomes the last
# renewal invoice for customer which will be used for any future comparisons.
LicenseLedger . objects . create (
plan = plan ,
is_renewal = True ,
event_time = event_time ,
licenses = licenses ,
licenses_at_next_renewal = licenses ,
)
# Update the last sent invoice with the new licenses. We just need to update `quantity` in
# the first invoice item. So, we void the current invoice and create a new copy of it with
# the updated quantity.
stripe_invoice = stripe . Invoice . retrieve ( last_sent_invoice . stripe_invoice_id )
assert stripe_invoice . status == " open "
2024-04-30 20:02:47 +02:00
assert isinstance ( stripe_invoice . customer , str )
assert stripe_invoice . statement_descriptor is not None
assert stripe_invoice . metadata is not None
2024-04-10 10:31:15 +02:00
invoice_items = stripe_invoice . lines . data
# Stripe does something weird and puts the discount item first, so we need to reverse the order here.
invoice_items . reverse ( )
for invoice_item in invoice_items :
2024-04-30 20:02:47 +02:00
assert invoice_item . description is not None
2024-04-30 19:23:16 +02:00
price_args : PriceArgs = { }
2024-04-10 10:31:15 +02:00
# If amount is positive, this must be non-discount item we need to update.
if invoice_item . amount > 0 :
assert invoice_item . price is not None
2024-04-30 20:02:47 +02:00
assert invoice_item . price . unit_amount is not None
2024-04-10 10:31:15 +02:00
price_args = {
" quantity " : licenses ,
" unit_amount " : invoice_item . price . unit_amount ,
}
else :
price_args = {
" amount " : invoice_item . amount ,
}
stripe . InvoiceItem . create (
currency = invoice_item . currency ,
customer = stripe_invoice . customer ,
description = invoice_item . description ,
2024-04-30 19:56:58 +02:00
period = {
" start " : invoice_item . period . start ,
" end " : invoice_item . period . end ,
} ,
2024-04-10 10:31:15 +02:00
* * price_args ,
)
assert plan . next_invoice_date is not None
# Difference between end of free trial and event time
days_until_due = ( plan . next_invoice_date - event_time ) . days
new_stripe_invoice = stripe . Invoice . create (
auto_advance = False ,
collection_method = " send_invoice " ,
customer = stripe_invoice . customer ,
days_until_due = days_until_due ,
statement_descriptor = stripe_invoice . statement_descriptor ,
metadata = stripe_invoice . metadata ,
)
new_stripe_invoice = stripe . Invoice . finalize_invoice ( new_stripe_invoice )
last_sent_invoice . stripe_invoice_id = str ( new_stripe_invoice . id )
last_sent_invoice . save ( update_fields = [ " stripe_invoice_id " ] )
assert stripe_invoice . id is not None
stripe . Invoice . void_invoice ( stripe_invoice . id )
2023-12-05 19:47:32 +01:00
def update_license_ledger_for_manual_plan (
self ,
plan : CustomerPlan ,
event_time : datetime ,
2024-07-12 02:30:23 +02:00
licenses : int | None = None ,
licenses_at_next_renewal : int | None = None ,
2023-12-05 19:47:32 +01:00
) - > None :
if licenses is not None :
if not plan . customer . exempt_from_license_number_check :
assert self . current_count_for_billed_licenses ( ) < = licenses
assert licenses > plan . licenses ( )
LicenseLedger . objects . create (
plan = plan ,
event_time = event_time ,
licenses = licenses ,
licenses_at_next_renewal = licenses ,
)
elif licenses_at_next_renewal is not None :
2023-12-12 14:18:38 +01:00
assert (
self . get_billable_licenses_for_customer (
plan . customer , plan . tier , licenses_at_next_renewal
)
< = licenses_at_next_renewal
)
2023-12-05 19:47:32 +01:00
LicenseLedger . objects . create (
plan = plan ,
event_time = event_time ,
licenses = plan . licenses ( ) ,
licenses_at_next_renewal = licenses_at_next_renewal ,
)
else :
raise AssertionError ( " Pass licenses or licenses_at_next_renewal " )
2023-12-12 14:18:38 +01:00
def get_billable_licenses_for_customer (
2023-12-15 10:27:50 +01:00
self ,
customer : Customer ,
tier : int ,
2024-07-12 02:30:23 +02:00
licenses : int | None = None ,
2023-12-15 10:27:50 +01:00
event_time : datetime = timezone_now ( ) ,
2023-12-12 14:18:38 +01:00
) - > int :
if licenses is not None and customer . exempt_from_license_number_check :
return licenses
2023-12-15 10:27:50 +01:00
current_licenses_count = self . current_count_for_billed_licenses ( event_time )
2023-12-12 14:18:38 +01:00
min_licenses_for_plan = self . min_licenses_for_plan ( tier )
if customer . exempt_from_license_number_check : # nocoverage
billed_licenses = current_licenses_count
else :
billed_licenses = max ( current_licenses_count , min_licenses_for_plan )
return billed_licenses
2023-12-05 21:09:28 +01:00
def update_license_ledger_for_automanaged_plan (
self , plan : CustomerPlan , event_time : datetime
2024-07-12 02:30:23 +02:00
) - > CustomerPlan | None :
2023-12-05 21:09:28 +01:00
new_plan , last_ledger_entry = self . make_end_of_cycle_updates_if_needed ( plan , event_time )
if last_ledger_entry is None :
2023-12-08 13:19:24 +01:00
return None
2023-12-05 21:09:28 +01:00
if new_plan is not None :
plan = new_plan
2023-12-12 14:18:38 +01:00
2023-12-12 14:51:23 +01:00
if plan . status == CustomerPlan . SWITCH_PLAN_TIER_AT_PLAN_END : # nocoverage
next_plan = self . get_next_plan ( plan )
assert next_plan is not None
licenses_at_next_renewal = self . get_billable_licenses_for_customer (
2023-12-15 10:27:50 +01:00
plan . customer ,
next_plan . tier ,
event_time = event_time ,
2023-12-12 14:51:23 +01:00
)
# Current licenses stay as per the limits of current plan.
current_plan_licenses_at_next_renewal = self . get_billable_licenses_for_customer (
2023-12-15 10:27:50 +01:00
plan . customer ,
plan . tier ,
event_time = event_time ,
2023-12-12 14:51:23 +01:00
)
licenses = max ( current_plan_licenses_at_next_renewal , last_ledger_entry . licenses )
else :
licenses_at_next_renewal = self . get_billable_licenses_for_customer (
2023-12-15 10:27:50 +01:00
plan . customer ,
plan . tier ,
event_time = event_time ,
2023-12-12 14:51:23 +01:00
)
licenses = max ( licenses_at_next_renewal , last_ledger_entry . licenses )
2023-12-05 21:09:28 +01:00
LicenseLedger . objects . create (
plan = plan ,
event_time = event_time ,
licenses = licenses ,
licenses_at_next_renewal = licenses_at_next_renewal ,
)
2023-12-08 13:19:24 +01:00
# Returning plan is particularly helpful for 'sync_license_ledger_if_needed'.
# If a new plan is created during the end of cycle update, then that function
# needs the updated plan for a correct LicenseLedger update.
return plan
2023-12-05 21:09:28 +01:00
2023-12-11 14:26:40 +01:00
def migrate_customer_to_legacy_plan (
self ,
renewal_date : datetime ,
end_date : datetime ,
2023-12-19 12:24:15 +01:00
) - > None :
2023-12-11 14:26:40 +01:00
assert not isinstance ( self , RealmBillingSession )
# Set stripe_customer_id to None to avoid customer being charged without a payment method.
customer = self . update_or_create_customer (
stripe_customer_id = None , defaults = { " stripe_customer_id " : None }
)
# Servers on legacy plan which are scheduled to be upgraded have 2 plans.
# This plan will be used to track the current status of SWITCH_PLAN_TIER_AT_PLAN_END
# and will not charge the customer. The other plan will be used to track the new plan
# customer will move to the end of this plan.
legacy_plan_anchor = renewal_date
legacy_plan_params = {
" billing_cycle_anchor " : legacy_plan_anchor ,
" status " : CustomerPlan . ACTIVE ,
" tier " : CustomerPlan . TIER_SELF_HOSTED_LEGACY ,
# End when the new plan starts.
" end_date " : end_date ,
2024-01-22 14:20:49 +01:00
" next_invoice_date " : end_date ,
2023-12-11 14:26:40 +01:00
# The primary mechanism for preventing charges under this
2024-01-22 14:20:49 +01:00
# plan is setting 'invoiced_through' to last ledger_entry below,
# but setting a 0 price is useful defense in depth here.
2023-12-11 14:26:40 +01:00
" price_per_license " : 0 ,
" billing_schedule " : CustomerPlan . BILLING_SCHEDULE_ANNUAL ,
" automanage_licenses " : True ,
}
legacy_plan = CustomerPlan . objects . create (
customer = customer ,
* * legacy_plan_params ,
)
2023-12-13 08:46:47 +01:00
try :
billed_licenses = self . get_billable_licenses_for_customer ( customer , legacy_plan . tier )
except MissingDataError :
billed_licenses = 0
2023-12-11 14:26:40 +01:00
# Create a ledger entry for the legacy plan for tracking purposes.
ledger_entry = LicenseLedger . objects . create (
plan = legacy_plan ,
is_renewal = True ,
event_time = legacy_plan_anchor ,
licenses = billed_licenses ,
licenses_at_next_renewal = billed_licenses ,
)
legacy_plan . invoiced_through = ledger_entry
legacy_plan . save ( update_fields = [ " invoiced_through " ] )
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . CUSTOMER_PLAN_CREATED ,
2023-12-11 14:26:40 +01:00
event_time = legacy_plan_anchor ,
extra_data = legacy_plan_params ,
)
2023-12-14 01:16:03 +01:00
self . do_change_plan_type ( tier = CustomerPlan . TIER_SELF_HOSTED_LEGACY , is_sponsored = False )
2023-12-18 21:09:33 +01:00
def add_customer_to_community_plan ( self ) - > None :
# There is no CustomerPlan for organizations on Zulip Cloud and
# they enjoy the same benefits as the Standard plan.
# For self-hosted organizations, sponsored organizations have
# a Community CustomerPlan and they have different benefits compared
# to customers on Business plan.
assert not isinstance ( self , RealmBillingSession )
customer = self . update_or_create_customer ( )
plan = get_current_plan_by_customer ( customer )
# Only plan that can be active is legacy plan. Which is already
# ended by the support path from which is this function is called.
assert plan is None
now = timezone_now ( )
community_plan_params = {
" billing_cycle_anchor " : now ,
" status " : CustomerPlan . ACTIVE ,
" tier " : CustomerPlan . TIER_SELF_HOSTED_COMMUNITY ,
# The primary mechanism for preventing charges under this
# plan is setting a null `next_invoice_date`, but setting
# a 0 price is useful defense in depth here.
" next_invoice_date " : None ,
" price_per_license " : 0 ,
" billing_schedule " : CustomerPlan . BILLING_SCHEDULE_ANNUAL ,
" automanage_licenses " : True ,
}
community_plan = CustomerPlan . objects . create (
customer = customer ,
* * community_plan_params ,
)
try :
billed_licenses = self . get_billable_licenses_for_customer ( customer , community_plan . tier )
except MissingDataError :
billed_licenses = 0
# Create a ledger entry for the community plan for tracking purposes.
# Also, since it is an active plan we need to it have at least one license ledger entry.
ledger_entry = LicenseLedger . objects . create (
plan = community_plan ,
is_renewal = True ,
event_time = now ,
licenses = billed_licenses ,
licenses_at_next_renewal = billed_licenses ,
)
community_plan . invoiced_through = ledger_entry
community_plan . save ( update_fields = [ " invoiced_through " ] )
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . CUSTOMER_PLAN_CREATED ,
2023-12-18 21:09:33 +01:00
event_time = now ,
extra_data = community_plan_params ,
)
2023-12-12 08:12:26 +01:00
def get_last_ledger_for_automanaged_plan_if_exists (
self ,
2024-07-12 02:30:23 +02:00
) - > LicenseLedger | None :
2023-12-12 08:12:26 +01:00
customer = self . get_customer ( )
if customer is None :
return None
plan = get_current_plan_by_customer ( customer )
if plan is None :
return None
if not plan . automanage_licenses :
return None
# It's an invariant that any current plan have at least an
# initial ledger entry.
last_ledger = LicenseLedger . objects . filter ( plan = plan ) . order_by ( " id " ) . last ( )
assert last_ledger is not None
return last_ledger
2023-10-26 14:11:43 +02:00
class RealmBillingSession ( BillingSession ) :
2023-11-23 17:34:58 +01:00
def __init__ (
self ,
2024-07-12 02:30:23 +02:00
user : UserProfile | None = None ,
realm : Realm | None = None ,
2023-11-23 17:34:58 +01:00
* ,
support_session : bool = False ,
) - > None :
2023-10-26 14:11:43 +02:00
self . user = user
2023-11-13 15:05:56 +01:00
assert user is not None or realm is not None
2023-11-23 17:34:58 +01:00
if support_session :
assert user is not None and user . is_staff
self . support_session = support_session
2023-11-13 15:05:56 +01:00
if user is not None and realm is not None :
2023-11-23 17:34:58 +01:00
assert user . is_staff or user . realm == realm
2023-10-31 19:22:55 +01:00
self . realm = realm
2023-11-13 15:05:56 +01:00
elif user is not None :
2023-10-31 19:22:55 +01:00
self . realm = user . realm
2023-11-13 15:05:56 +01:00
else :
assert realm is not None # for mypy
self . realm = realm
2023-10-26 14:11:43 +02:00
2023-11-27 17:31:39 +01:00
PAID_PLANS = [
Realm . PLAN_TYPE_STANDARD ,
Realm . PLAN_TYPE_PLUS ,
]
2023-11-30 21:11:54 +01:00
@override
@property
def billing_entity_display_name ( self ) - > str :
return self . realm . string_id
2023-11-06 13:52:12 +01:00
@override
@property
def billing_session_url ( self ) - > str :
2024-05-06 15:27:22 +02:00
return self . realm . url
2023-11-06 13:52:12 +01:00
2023-12-01 04:18:58 +01:00
@override
@property
def billing_base_url ( self ) - > str :
return " "
2023-11-30 01:48:46 +01:00
@override
def support_url ( self ) - > str :
2023-12-06 15:00:23 +01:00
return build_support_url ( " support " , self . realm . string_id )
2023-11-30 01:48:46 +01:00
2023-10-26 14:11:43 +02:00
@override
2024-07-12 02:30:23 +02:00
def get_customer ( self ) - > Customer | None :
2023-10-26 14:11:43 +02:00
return get_customer_by_realm ( self . realm )
2023-12-02 03:54:24 +01:00
@override
def get_email ( self ) - > str :
assert self . user is not None
return self . user . delivery_email
2023-11-08 17:02:31 +01:00
@override
2023-12-08 13:19:24 +01:00
def current_count_for_billed_licenses ( self , event_time : datetime = timezone_now ( ) ) - > int :
2023-11-08 17:02:31 +01:00
return get_latest_seat_count ( self . realm )
2023-11-02 17:44:02 +01:00
@override
2024-08-29 20:48:40 +02:00
def get_audit_log_event ( self , event_type : BillingSessionEventType ) - > int :
if event_type is BillingSessionEventType . STRIPE_CUSTOMER_CREATED :
2023-11-02 17:44:02 +01:00
return RealmAuditLog . STRIPE_CUSTOMER_CREATED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . STRIPE_CARD_CHANGED :
2023-11-02 17:44:02 +01:00
return RealmAuditLog . STRIPE_CARD_CHANGED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . CUSTOMER_PLAN_CREATED :
2023-11-02 17:44:02 +01:00
return RealmAuditLog . CUSTOMER_PLAN_CREATED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . DISCOUNT_CHANGED :
2023-11-02 17:44:02 +01:00
return RealmAuditLog . REALM_DISCOUNT_CHANGED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . CUSTOMER_PROPERTY_CHANGED :
2024-01-12 17:38:55 +01:00
return RealmAuditLog . CUSTOMER_PROPERTY_CHANGED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . SPONSORSHIP_APPROVED :
2023-11-02 15:23:35 +01:00
return RealmAuditLog . REALM_SPONSORSHIP_APPROVED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . SPONSORSHIP_PENDING_STATUS_CHANGED :
2023-11-02 18:17:08 +01:00
return RealmAuditLog . REALM_SPONSORSHIP_PENDING_STATUS_CHANGED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . BILLING_MODALITY_CHANGED :
2023-12-01 13:19:04 +01:00
return RealmAuditLog . REALM_BILLING_MODALITY_CHANGED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . CUSTOMER_PLAN_PROPERTY_CHANGED :
2024-01-12 17:38:55 +01:00
return RealmAuditLog . CUSTOMER_PLAN_PROPERTY_CHANGED # nocoverage
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN :
2023-11-13 15:05:56 +01:00
return RealmAuditLog . CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN :
2023-11-20 13:01:25 +01:00
return RealmAuditLog . CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN
2023-11-02 17:44:02 +01:00
else :
raise BillingSessionAuditLogEventError ( event_type )
2023-10-26 14:11:43 +02:00
@override
def write_to_audit_log (
2023-11-02 17:44:02 +01:00
self ,
2024-08-29 20:48:40 +02:00
event_type : BillingSessionEventType ,
2023-11-02 17:44:02 +01:00
event_time : datetime ,
* ,
2023-12-24 15:56:33 +01:00
background_update : bool = False ,
2024-07-12 02:30:23 +02:00
extra_data : dict [ str , Any ] | None = None ,
2023-10-26 14:11:43 +02:00
) - > None :
2023-11-02 17:44:02 +01:00
audit_log_event = self . get_audit_log_event ( event_type )
2023-11-13 15:05:56 +01:00
audit_log_data = {
" realm " : self . realm ,
" event_type " : audit_log_event ,
" event_time " : event_time ,
}
2023-10-26 14:11:43 +02:00
if extra_data :
2023-11-13 15:05:56 +01:00
audit_log_data [ " extra_data " ] = extra_data
2023-12-24 15:56:33 +01:00
if self . user is not None and not background_update :
2023-11-13 15:05:56 +01:00
audit_log_data [ " acting_user " ] = self . user
RealmAuditLog . objects . create ( * * audit_log_data )
2023-10-26 14:11:43 +02:00
@override
2023-10-31 15:51:51 +01:00
def get_data_for_stripe_customer ( self ) - > StripeCustomerData :
2023-10-31 19:22:55 +01:00
# Support requests do not set any stripe billing information.
assert self . support_session is False
2023-11-13 15:05:56 +01:00
assert self . user is not None
2024-07-12 02:30:17 +02:00
metadata : dict [ str , Any ] = { }
2023-10-31 15:51:51 +01:00
metadata [ " realm_id " ] = self . realm . id
metadata [ " realm_str " ] = self . realm . string_id
realm_stripe_customer_data = StripeCustomerData (
description = f " { self . realm . string_id } ( { self . realm . name } ) " ,
2023-12-02 03:54:24 +01:00
email = self . get_email ( ) ,
2023-10-31 15:51:51 +01:00
metadata = metadata ,
)
return realm_stripe_customer_data
2023-10-26 14:11:43 +02:00
2023-11-06 15:51:54 +01:00
@override
2024-02-10 07:47:32 +01:00
def update_data_for_checkout_session_and_invoice_payment (
2024-07-12 02:30:17 +02:00
self , metadata : dict [ str , Any ]
) - > dict [ str , Any ] :
2023-11-13 15:05:56 +01:00
assert self . user is not None
2023-11-06 15:51:54 +01:00
updated_metadata = dict (
2023-12-02 03:54:24 +01:00
user_email = self . get_email ( ) ,
2023-11-06 15:51:54 +01:00
realm_id = self . realm . id ,
realm_str = self . realm . string_id ,
user_id = self . user . id ,
* * metadata ,
)
return updated_metadata
2023-10-26 14:11:43 +02:00
@override
2023-10-31 19:22:55 +01:00
def update_or_create_customer (
2024-07-12 02:30:23 +02:00
self , stripe_customer_id : str | None = None , * , defaults : dict [ str , Any ] | None = None
2023-10-31 19:22:55 +01:00
) - > Customer :
if stripe_customer_id is not None :
# Support requests do not set any stripe billing information.
assert self . support_session is False
customer , created = Customer . objects . update_or_create (
realm = self . realm , defaults = { " stripe_customer_id " : stripe_customer_id }
)
2023-11-15 22:30:08 +01:00
from zerver . actions . users import do_change_is_billing_admin
2021-05-28 12:36:41 +02:00
2023-11-13 15:05:56 +01:00
assert self . user is not None
2023-11-15 22:30:08 +01:00
do_change_is_billing_admin ( self . user , True )
2023-10-31 19:22:55 +01:00
return customer
else :
customer , created = Customer . objects . update_or_create (
realm = self . realm , defaults = defaults
)
return customer
2018-08-14 03:33:31 +02:00
2023-11-08 17:15:40 +01:00
@override
2023-12-24 15:56:33 +01:00
def do_change_plan_type (
2024-07-12 02:30:23 +02:00
self , * , tier : int | None , is_sponsored : bool = False , background_update : bool = False
2023-12-24 15:56:33 +01:00
) - > None :
2023-11-08 17:15:40 +01:00
from zerver . actions . realm_settings import do_change_realm_plan_type
# This function needs to translate between the different
# formats of CustomerPlan.tier and Realm.plan_type.
if is_sponsored :
2023-12-18 21:09:33 +01:00
# Cloud sponsored customers don't have an active CustomerPlan.
2023-11-08 17:15:40 +01:00
plan_type = Realm . PLAN_TYPE_STANDARD_FREE
2023-11-30 07:43:06 +01:00
elif tier == CustomerPlan . TIER_CLOUD_STANDARD :
2023-11-08 17:15:40 +01:00
plan_type = Realm . PLAN_TYPE_STANDARD
2024-04-16 10:19:00 +02:00
elif tier == CustomerPlan . TIER_CLOUD_PLUS :
2023-11-08 17:15:40 +01:00
plan_type = Realm . PLAN_TYPE_PLUS
else :
raise AssertionError ( " Unexpected tier " )
2023-12-24 15:56:33 +01:00
acting_user = None
if not background_update :
acting_user = self . user
do_change_realm_plan_type ( self . realm , plan_type , acting_user = acting_user )
2023-11-08 17:15:40 +01:00
2023-11-13 15:05:56 +01:00
@override
2023-12-24 15:56:33 +01:00
def process_downgrade ( self , plan : CustomerPlan , background_update : bool = False ) - > None :
2023-11-13 15:05:56 +01:00
from zerver . actions . realm_settings import do_change_realm_plan_type
2023-12-24 15:56:33 +01:00
acting_user = None
if not background_update :
acting_user = self . user
2023-11-13 15:05:56 +01:00
assert plan . customer . realm is not None
2023-12-24 15:56:33 +01:00
do_change_realm_plan_type (
plan . customer . realm , Realm . PLAN_TYPE_LIMITED , acting_user = acting_user
)
2023-11-13 15:05:56 +01:00
plan . status = CustomerPlan . ENDED
plan . save ( update_fields = [ " status " ] )
2023-11-02 15:23:35 +01:00
@override
2023-11-30 21:11:54 +01:00
def approve_sponsorship ( self ) - > str :
2023-11-02 15:23:35 +01:00
# Sponsorship approval is only a support admin action.
assert self . support_session
2023-12-13 10:21:48 +01:00
customer = self . get_customer ( )
if customer is not None :
error_message = self . check_customer_not_on_paid_plan ( customer )
2024-01-03 20:22:49 +01:00
if error_message != " " :
raise SupportRequestError ( error_message )
2023-12-13 10:21:48 +01:00
2023-11-02 15:23:35 +01:00
from zerver . actions . message_send import internal_send_private_message
2024-08-14 03:49:42 +02:00
if self . realm . deactivated :
raise SupportRequestError ( " Realm has been deactivated " )
2023-11-08 17:15:40 +01:00
self . do_change_plan_type ( tier = None , is_sponsored = True )
2023-11-02 15:23:35 +01:00
if customer is not None and customer . sponsorship_pending :
customer . sponsorship_pending = False
customer . save ( update_fields = [ " sponsorship_pending " ] )
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . SPONSORSHIP_APPROVED , event_time = timezone_now ( )
2023-11-02 15:23:35 +01:00
)
notification_bot = get_system_bot ( settings . NOTIFICATION_BOT , self . realm . id )
for user in self . realm . get_human_billing_admin_and_realm_owner_users ( ) :
with override_language ( user . default_language ) :
# Using variable to make life easier for translators if these details change.
message = _ (
" Your organization ' s request for sponsored hosting has been approved! "
" You have been upgraded to {plan_name} , free of charge. {emoji} \n \n "
" If you could {begin_link} list Zulip as a sponsor on your website {end_link} , "
" we would really appreciate it! "
) . format (
2023-12-12 19:57:27 +01:00
plan_name = CustomerPlan . name_from_tier ( CustomerPlan . TIER_CLOUD_STANDARD ) ,
2023-11-02 15:23:35 +01:00
emoji = " :tada: " ,
begin_link = " [ " ,
end_link = " ](/help/linking-to-zulip-website) " ,
)
internal_send_private_message ( notification_bot , user , message )
2024-08-19 12:03:56 +02:00
return f " Sponsorship approved for { self . billing_entity_display_name } ; Emailed organization owners and billing admins. "
2023-11-02 15:23:35 +01:00
2023-11-27 13:25:11 +01:00
@override
def is_sponsored ( self ) - > bool :
return self . realm . plan_type == self . realm . PLAN_TYPE_STANDARD_FREE
2023-12-01 03:51:05 +01:00
@override
2024-07-12 02:30:17 +02:00
def get_metadata_for_stripe_update_card ( self ) - > dict [ str , str ] :
2023-12-01 03:51:05 +01:00
assert self . user is not None
return {
" type " : " card_update " ,
2024-04-30 19:20:47 +02:00
" user_id " : str ( self . user . id ) ,
2023-12-01 03:51:05 +01:00
}
2023-11-20 08:40:09 +01:00
@override
2023-11-24 07:29:06 +01:00
def get_upgrade_page_session_type_specific_context (
self ,
) - > UpgradePageSessionTypeSpecificContext :
2023-11-20 08:40:09 +01:00
assert self . user is not None
2023-11-24 07:29:06 +01:00
return UpgradePageSessionTypeSpecificContext (
customer_name = self . realm . name ,
2023-12-02 03:54:24 +01:00
email = self . get_email ( ) ,
2023-11-24 07:29:06 +01:00
is_demo_organization = self . realm . demo_organization_scheduled_deletion_date is not None ,
demo_organization_scheduled_deletion_date = self . realm . demo_organization_scheduled_deletion_date ,
is_self_hosting = False ,
)
2023-11-20 08:40:09 +01:00
2024-01-10 17:20:08 +01:00
@override
def check_plan_tier_is_billable ( self , plan_tier : int ) - > bool :
implemented_plan_tiers = [
CustomerPlan . TIER_CLOUD_STANDARD ,
CustomerPlan . TIER_CLOUD_PLUS ,
]
if plan_tier in implemented_plan_tiers :
return True
return False
2023-11-23 07:29:03 +01:00
@override
2023-11-30 17:11:41 +01:00
def get_type_of_plan_tier_change (
self , current_plan_tier : int , new_plan_tier : int
) - > PlanTierChangeType :
valid_plan_tiers = [ CustomerPlan . TIER_CLOUD_STANDARD , CustomerPlan . TIER_CLOUD_PLUS ]
if (
current_plan_tier not in valid_plan_tiers
or new_plan_tier not in valid_plan_tiers
or current_plan_tier == new_plan_tier
) :
return PlanTierChangeType . INVALID
if (
current_plan_tier == CustomerPlan . TIER_CLOUD_STANDARD
and new_plan_tier == CustomerPlan . TIER_CLOUD_PLUS
) :
return PlanTierChangeType . UPGRADE
2023-11-23 07:29:03 +01:00
else : # nocoverage, not currently implemented
2023-11-30 07:43:06 +01:00
assert current_plan_tier == CustomerPlan . TIER_CLOUD_PLUS
2023-11-30 17:11:41 +01:00
assert new_plan_tier == CustomerPlan . TIER_CLOUD_STANDARD
return PlanTierChangeType . DOWNGRADE
2023-11-23 07:29:03 +01:00
2023-11-27 11:07:03 +01:00
@override
def has_billing_access ( self ) - > bool :
assert self . user is not None
return self . user . has_billing_access
2023-11-27 13:08:43 +01:00
@override
def on_paid_plan ( self ) - > bool :
2023-11-27 17:31:39 +01:00
return self . realm . plan_type in self . PAID_PLANS
2023-11-27 13:08:43 +01:00
2024-02-01 05:07:01 +01:00
@override
def org_name ( self ) - > str :
return self . realm . name
2023-11-27 13:08:43 +01:00
@override
2024-07-12 02:30:17 +02:00
def add_sponsorship_info_to_context ( self , context : dict [ str , Any ] ) - > None :
2023-11-27 13:08:43 +01:00
context . update (
realm_org_type = self . 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 " )
) ,
2023-12-01 03:50:13 +01:00
key = sponsorship_org_type_key_helper ,
2023-11-27 13:08:43 +01:00
) ,
)
2023-11-30 01:48:46 +01:00
@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 ,
2023-12-02 03:54:24 +01:00
email = self . get_email ( ) ,
2023-11-30 01:48:46 +01:00
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 " ] )
2023-12-08 13:19:24 +01:00
def update_license_ledger_if_needed ( self , event_time : datetime ) - > None :
customer = self . get_customer ( )
if customer is None :
return
plan = get_current_plan_by_customer ( customer )
if plan is None :
return
if not plan . automanage_licenses :
return
self . update_license_ledger_for_automanaged_plan ( plan , event_time )
2023-12-12 09:02:17 +01:00
@override
def sync_license_ledger_if_needed ( self ) - > None : # nocoverage
# TODO: For zulip cloud, currently we use 'update_license_ledger_if_needed'
# to update the ledger. For consistency, we plan to use RealmAuditlog
# to update the ledger as we currently do for self-hosted system using
# RemoteRealmAuditlog. This will also help the cloud billing system to
# recover from a multi-day outage of the invoicing process without doing
# anything weird.
pass
2021-02-12 08:19:30 +01:00
2023-12-13 05:49:15 +01:00
class RemoteRealmBillingSession ( BillingSession ) :
2023-11-09 20:40:42 +01:00
def __init__ (
2023-11-30 01:48:46 +01:00
self ,
remote_realm : RemoteRealm ,
2024-07-12 02:30:23 +02:00
remote_billing_user : RemoteRealmBillingUser | None = None ,
support_staff : UserProfile | None = None ,
2023-11-09 20:40:42 +01:00
) - > None :
self . remote_realm = remote_realm
2023-12-10 20:05:43 +01:00
self . remote_billing_user = remote_billing_user
2023-12-14 15:50:12 +01:00
self . support_staff = support_staff
2023-12-13 05:49:15 +01:00
if support_staff is not None : # nocoverage
2023-11-09 20:40:42 +01:00
assert support_staff . is_staff
self . support_session = True
else :
self . support_session = False
2023-11-30 21:11:54 +01:00
@override
@property
2023-12-13 05:49:15 +01:00
def billing_entity_display_name ( self ) - > str : # nocoverage
2023-11-30 21:11:54 +01:00
return self . remote_realm . name
2023-11-09 20:40:42 +01:00
@override
@property
2023-12-13 05:49:15 +01:00
def billing_session_url ( self ) - > str : # nocoverage
2023-11-24 09:08:24 +01:00
return f " { settings . EXTERNAL_URI_SCHEME } { settings . SELF_HOSTING_MANAGEMENT_SUBDOMAIN } . { settings . EXTERNAL_HOST } /realm/ { self . remote_realm . uuid } "
2023-11-09 20:40:42 +01:00
2023-12-01 04:18:58 +01:00
@override
@property
def billing_base_url ( self ) - > str :
return f " /realm/ { self . remote_realm . uuid } "
2023-11-30 01:48:46 +01:00
@override
2023-12-13 05:49:15 +01:00
def support_url ( self ) - > str : # nocoverage
2024-02-28 21:25:12 +01:00
return build_support_url ( " remote_servers_support " , str ( self . remote_realm . uuid ) )
2023-11-30 01:48:46 +01:00
2023-11-09 20:40:42 +01:00
@override
2024-07-12 02:30:23 +02:00
def get_customer ( self ) - > Customer | None :
2023-11-09 20:40:42 +01:00
return get_customer_by_remote_realm ( self . remote_realm )
2023-12-02 03:54:24 +01:00
@override
def get_email ( self ) - > str :
2023-12-12 19:57:27 +01:00
assert self . remote_billing_user is not None
return self . remote_billing_user . email
2023-12-02 03:54:24 +01:00
2023-11-09 20:40:42 +01:00
@override
2023-12-08 13:19:24 +01:00
def current_count_for_billed_licenses ( self , event_time : datetime = timezone_now ( ) ) - > int :
2023-12-06 19:25:49 +01:00
if has_stale_audit_log ( self . remote_realm . server ) :
raise MissingDataError
2023-12-08 13:19:24 +01:00
remote_realm_counts = get_remote_realm_guest_and_non_guest_count (
self . remote_realm , event_time
)
2023-12-06 14:26:07 +01:00
return remote_realm_counts . non_guest_user_count + remote_realm_counts . guest_user_count
2023-11-09 20:40:42 +01:00
2023-12-13 05:49:15 +01:00
def missing_data_error_page ( self , request : HttpRequest ) - > HttpResponse : # nocoverage
2023-12-07 15:27:39 +01:00
# The RemoteRealm error page code path should not really be
# possible, in that the self-hosted server will have uploaded
# current audit log data as needed as part of logging the user
# in.
2024-07-12 02:30:17 +02:00
missing_data_context : dict [ str , Any ] = {
2023-12-07 15:27:39 +01:00
" remote_realm_session " : True ,
" supports_remote_realms " : self . remote_realm . server . last_api_feature_level is not None ,
}
return render (
2024-01-29 15:46:18 +01:00
request ,
" corporate/billing/server_not_uploading_data.html " ,
context = missing_data_context ,
2023-12-07 15:27:39 +01:00
)
2023-11-09 20:40:42 +01:00
@override
2024-08-29 20:48:40 +02:00
def get_audit_log_event ( self , event_type : BillingSessionEventType ) - > int :
if event_type is BillingSessionEventType . STRIPE_CUSTOMER_CREATED :
2023-11-09 20:40:42 +01:00
return RemoteRealmAuditLog . STRIPE_CUSTOMER_CREATED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . STRIPE_CARD_CHANGED :
2023-11-09 20:40:42 +01:00
return RemoteRealmAuditLog . STRIPE_CARD_CHANGED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . CUSTOMER_PLAN_CREATED :
2023-11-09 20:40:42 +01:00
return RemoteRealmAuditLog . CUSTOMER_PLAN_CREATED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . DISCOUNT_CHANGED :
2023-11-09 20:40:42 +01:00
return RemoteRealmAuditLog . REMOTE_SERVER_DISCOUNT_CHANGED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . CUSTOMER_PROPERTY_CHANGED :
2024-01-12 17:38:55 +01:00
return RemoteRealmAuditLog . CUSTOMER_PROPERTY_CHANGED # nocoverage
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . SPONSORSHIP_APPROVED :
2023-11-09 20:40:42 +01:00
return RemoteRealmAuditLog . REMOTE_SERVER_SPONSORSHIP_APPROVED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . SPONSORSHIP_PENDING_STATUS_CHANGED :
2023-11-09 20:40:42 +01:00
return RemoteRealmAuditLog . REMOTE_SERVER_SPONSORSHIP_PENDING_STATUS_CHANGED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . BILLING_MODALITY_CHANGED :
2023-12-19 12:24:15 +01:00
return RemoteRealmAuditLog . REMOTE_SERVER_BILLING_MODALITY_CHANGED # nocoverage
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . CUSTOMER_PLAN_PROPERTY_CHANGED :
2024-01-12 17:38:55 +01:00
return RemoteRealmAuditLog . CUSTOMER_PLAN_PROPERTY_CHANGED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . BILLING_ENTITY_PLAN_TYPE_CHANGED :
2023-12-04 23:20:49 +01:00
return RemoteRealmAuditLog . REMOTE_SERVER_PLAN_TYPE_CHANGED
2023-12-19 12:24:15 +01:00
elif (
2024-08-29 20:48:40 +02:00
event_type is BillingSessionEventType . CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN
2023-12-19 12:24:15 +01:00
) : # nocoverage
2023-12-08 02:59:35 +01:00
return RemoteRealmAuditLog . CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN
2023-12-19 12:24:15 +01:00
elif (
2024-08-29 20:48:40 +02:00
event_type is BillingSessionEventType . CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN
2023-12-19 12:24:15 +01:00
) : # nocoverage
2023-12-08 02:59:35 +01:00
return RemoteRealmAuditLog . CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN
2023-12-19 12:24:15 +01:00
else : # nocoverage
2023-11-09 20:40:42 +01:00
raise BillingSessionAuditLogEventError ( event_type )
@override
def write_to_audit_log (
self ,
2024-08-29 20:48:40 +02:00
event_type : BillingSessionEventType ,
2023-11-09 20:40:42 +01:00
event_time : datetime ,
* ,
2023-12-24 15:56:33 +01:00
background_update : bool = False ,
2024-07-12 02:30:23 +02:00
extra_data : dict [ str , Any ] | None = None ,
2023-12-19 12:24:15 +01:00
) - > None :
2023-11-30 12:24:29 +01:00
# These audit logs don't use all the fields of `RemoteRealmAuditLog`:
#
# * remote_id is None because this is not synced from a remote table.
# * realm_id is None because we do not aim to store both remote_realm
# and the legacy realm_id field.
2023-11-09 20:40:42 +01:00
audit_log_event = self . get_audit_log_event ( event_type )
2023-11-30 12:20:45 +01:00
log_data = {
" server " : self . remote_realm . server ,
" remote_realm " : self . remote_realm ,
" event_type " : audit_log_event ,
" event_time " : event_time ,
}
2023-12-24 15:56:33 +01:00
if not background_update :
log_data . update (
{
# At most one of these should be set, but we may
# not want an assert for that yet:
" acting_support_user " : self . support_staff ,
" acting_remote_user " : self . remote_billing_user ,
}
)
2023-11-09 20:40:42 +01:00
if extra_data :
2023-11-30 12:20:45 +01:00
log_data [ " extra_data " ] = extra_data
RemoteRealmAuditLog . objects . create ( * * log_data )
2023-11-09 20:40:42 +01:00
@override
2023-12-19 12:24:15 +01:00
def get_data_for_stripe_customer ( self ) - > StripeCustomerData :
2023-11-09 20:40:42 +01:00
# Support requests do not set any stripe billing information.
assert self . support_session is False
2024-07-12 02:30:17 +02:00
metadata : dict [ str , Any ] = { }
2023-11-09 20:40:42 +01:00
metadata [ " remote_realm_uuid " ] = self . remote_realm . uuid
metadata [ " remote_realm_host " ] = str ( self . remote_realm . host )
realm_stripe_customer_data = StripeCustomerData (
description = str ( self . remote_realm ) ,
2023-12-02 03:54:24 +01:00
email = self . get_email ( ) ,
2023-11-09 20:40:42 +01:00
metadata = metadata ,
)
return realm_stripe_customer_data
@override
2024-02-10 07:47:32 +01:00
def update_data_for_checkout_session_and_invoice_payment (
2024-07-12 02:30:17 +02:00
self , metadata : dict [ str , Any ]
) - > dict [ str , Any ] :
2023-12-13 10:41:23 +01:00
assert self . remote_billing_user is not None
2023-11-09 20:40:42 +01:00
updated_metadata = dict (
2023-12-13 10:41:23 +01:00
remote_realm_user_id = self . remote_billing_user . id ,
remote_realm_user_email = self . get_email ( ) ,
remote_realm_host = self . remote_realm . host ,
2023-11-09 20:40:42 +01:00
* * metadata ,
)
return updated_metadata
@override
def update_or_create_customer (
2024-07-12 02:30:23 +02:00
self , stripe_customer_id : str | None = None , * , defaults : dict [ str , Any ] | None = None
2023-12-19 12:24:15 +01:00
) - > Customer :
2023-11-09 20:40:42 +01:00
if stripe_customer_id is not None :
# Support requests do not set any stripe billing information.
assert self . support_session is False
customer , created = Customer . objects . update_or_create (
remote_realm = self . remote_realm ,
defaults = { " stripe_customer_id " : stripe_customer_id } ,
)
else :
customer , created = Customer . objects . update_or_create (
remote_realm = self . remote_realm , defaults = defaults
)
2023-12-20 07:24:21 +01:00
2024-05-06 06:12:15 +02:00
if (
created
and not customer . annual_discounted_price
and not customer . monthly_discounted_price
) :
2023-12-20 07:24:21 +01:00
customer . flat_discounted_months = 12
customer . save ( update_fields = [ " flat_discounted_months " ] )
return customer
2023-11-09 20:40:42 +01:00
@override
2023-12-18 21:09:33 +01:00
@transaction.atomic
2023-12-13 05:49:15 +01:00
def do_change_plan_type (
2024-07-12 02:30:23 +02:00
self , * , tier : int | None , is_sponsored : bool = False , background_update : bool = False
2023-12-13 05:49:15 +01:00
) - > None : # nocoverage
2023-11-09 20:40:42 +01:00
if is_sponsored :
plan_type = RemoteRealm . PLAN_TYPE_COMMUNITY
2023-12-18 21:09:33 +01:00
self . add_customer_to_community_plan ( )
2023-12-18 23:57:32 +01:00
elif tier == CustomerPlan . TIER_SELF_HOSTED_BASIC :
plan_type = RemoteRealm . PLAN_TYPE_BASIC
2023-12-02 04:21:50 +01:00
elif tier == CustomerPlan . TIER_SELF_HOSTED_BUSINESS :
2023-11-09 20:40:42 +01:00
plan_type = RemoteRealm . PLAN_TYPE_BUSINESS
2023-12-14 01:16:03 +01:00
elif tier == CustomerPlan . TIER_SELF_HOSTED_LEGACY :
plan_type = RemoteRealm . PLAN_TYPE_SELF_MANAGED_LEGACY
2023-11-09 20:40:42 +01:00
else :
raise AssertionError ( " Unexpected tier " )
2023-12-12 19:57:27 +01:00
old_plan_type = self . remote_realm . plan_type
2023-11-09 20:40:42 +01:00
self . remote_realm . plan_type = plan_type
self . remote_realm . save ( update_fields = [ " plan_type " ] )
2023-12-12 19:57:27 +01:00
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . BILLING_ENTITY_PLAN_TYPE_CHANGED ,
2023-12-12 19:57:27 +01:00
event_time = timezone_now ( ) ,
extra_data = { " old_value " : old_plan_type , " new_value " : plan_type } ,
2023-12-24 15:56:33 +01:00
background_update = background_update ,
2023-12-12 19:57:27 +01:00
)
2023-11-09 20:40:42 +01:00
@override
2023-12-13 05:49:15 +01:00
def approve_sponsorship ( self ) - > str : # nocoverage
2023-12-02 18:21:04 +01:00
# Sponsorship approval is only a support admin action.
assert self . support_session
customer = self . get_customer ( )
2023-12-13 10:21:48 +01:00
if customer is not None :
error_message = self . check_customer_not_on_paid_plan ( customer )
if error_message != " " :
2024-01-03 20:22:49 +01:00
raise SupportRequestError ( error_message )
2023-12-13 10:21:48 +01:00
2023-12-14 05:35:11 +01:00
if self . remote_realm . plan_type == RemoteRealm . PLAN_TYPE_SELF_MANAGED_LEGACY :
plan = get_current_plan_by_customer ( customer )
# Ideally we should have always have a plan here but since this is support page, we can be lenient about it.
if plan is not None :
assert self . get_next_plan ( plan ) is None
assert plan . tier == CustomerPlan . TIER_SELF_HOSTED_LEGACY
plan . status = CustomerPlan . ENDED
plan . save ( update_fields = [ " status " ] )
2023-12-13 10:21:48 +01:00
self . do_change_plan_type ( tier = None , is_sponsored = True )
2023-12-02 18:21:04 +01:00
if customer is not None and customer . sponsorship_pending :
customer . sponsorship_pending = False
customer . save ( update_fields = [ " sponsorship_pending " ] )
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . SPONSORSHIP_APPROVED , event_time = timezone_now ( )
2023-12-02 18:21:04 +01:00
)
2023-12-07 17:01:29 +01:00
emailed_string = " "
billing_emails = list (
RemoteRealmBillingUser . objects . filter ( remote_realm_id = self . remote_realm . id ) . values_list (
" email " , flat = True
)
)
if len ( billing_emails ) > 0 :
send_email (
" zerver/emails/sponsorship_approved_community_plan " ,
to_emails = billing_emails ,
2023-12-14 13:45:31 +01:00
from_address = BILLING_SUPPORT_EMAIL ,
2023-12-07 17:01:29 +01:00
context = {
" billing_entity " : self . billing_entity_display_name ,
" plans_link " : " https://zulip.com/plans/#self-hosted " ,
" link_to_zulip " : " https://zulip.com/help/linking-to-zulip-website " ,
} ,
)
emailed_string = " Emailed existing billing users. "
else :
emailed_string = " No billing users exist to email. "
return f " Sponsorship approved for { self . billing_entity_display_name } ; " + emailed_string
2023-11-09 20:40:42 +01:00
2023-11-27 13:25:11 +01:00
@override
def is_sponsored ( self ) - > bool :
return self . remote_realm . plan_type == self . remote_realm . PLAN_TYPE_COMMUNITY
2023-12-01 03:51:05 +01:00
@override
2024-07-12 02:30:17 +02:00
def get_metadata_for_stripe_update_card ( self ) - > dict [ str , str ] : # nocoverage
2023-12-12 19:57:27 +01:00
assert self . remote_billing_user is not None
return { " type " : " card_update " , " remote_realm_user_id " : str ( self . remote_billing_user . id ) }
2023-12-01 03:51:05 +01:00
2023-11-24 07:29:06 +01:00
@override
def get_upgrade_page_session_type_specific_context (
self ,
) - > UpgradePageSessionTypeSpecificContext :
return UpgradePageSessionTypeSpecificContext (
customer_name = self . remote_realm . host ,
2023-12-02 03:54:24 +01:00
email = self . get_email ( ) ,
2023-11-24 07:29:06 +01:00
is_demo_organization = False ,
demo_organization_scheduled_deletion_date = None ,
is_self_hosting = True ,
)
2023-11-09 20:40:42 +01:00
@override
2023-12-24 15:56:33 +01:00
def process_downgrade (
self , plan : CustomerPlan , background_update : bool = False
) - > None : # nocoverage
2023-12-04 23:20:49 +01:00
with transaction . atomic ( ) :
old_plan_type = self . remote_realm . plan_type
2023-12-14 00:17:55 +01:00
new_plan_type = RemoteRealm . PLAN_TYPE_SELF_MANAGED
2023-12-04 23:20:49 +01:00
self . remote_realm . plan_type = new_plan_type
self . remote_realm . save ( update_fields = [ " plan_type " ] )
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . BILLING_ENTITY_PLAN_TYPE_CHANGED ,
2023-12-04 23:20:49 +01:00
event_time = timezone_now ( ) ,
extra_data = { " old_value " : old_plan_type , " new_value " : new_plan_type } ,
2023-12-24 15:56:33 +01:00
background_update = background_update ,
2023-12-04 23:20:49 +01:00
)
2023-11-09 20:40:42 +01:00
plan . status = CustomerPlan . ENDED
plan . save ( update_fields = [ " status " ] )
2024-01-10 17:20:08 +01:00
@override
def check_plan_tier_is_billable ( self , plan_tier : int ) - > bool : # nocoverage
implemented_plan_tiers = [
CustomerPlan . TIER_SELF_HOSTED_BASIC ,
CustomerPlan . TIER_SELF_HOSTED_BUSINESS ,
]
if plan_tier in implemented_plan_tiers :
return True
return False
2023-11-23 07:29:03 +01:00
@override
2023-11-30 17:11:41 +01:00
def get_type_of_plan_tier_change (
self , current_plan_tier : int , new_plan_tier : int
2023-12-13 05:49:15 +01:00
) - > PlanTierChangeType : # nocoverage
2023-12-04 14:17:28 +01:00
valid_plan_tiers = [
2023-12-11 18:00:42 +01:00
CustomerPlan . TIER_SELF_HOSTED_LEGACY ,
2023-12-18 23:57:32 +01:00
CustomerPlan . TIER_SELF_HOSTED_BASIC ,
2023-12-04 14:17:28 +01:00
CustomerPlan . TIER_SELF_HOSTED_BUSINESS ,
]
if (
current_plan_tier not in valid_plan_tiers
or new_plan_tier not in valid_plan_tiers
or current_plan_tier == new_plan_tier
) :
return PlanTierChangeType . INVALID
if (
2023-12-18 23:57:32 +01:00
current_plan_tier == CustomerPlan . TIER_SELF_HOSTED_BASIC
and new_plan_tier == CustomerPlan . TIER_SELF_HOSTED_BUSINESS
2023-12-04 14:17:28 +01:00
) :
return PlanTierChangeType . UPGRADE
2023-12-11 18:00:42 +01:00
elif current_plan_tier == CustomerPlan . TIER_SELF_HOSTED_LEGACY and new_plan_tier in (
2023-12-18 23:57:32 +01:00
CustomerPlan . TIER_SELF_HOSTED_BASIC ,
2023-12-11 18:00:42 +01:00
CustomerPlan . TIER_SELF_HOSTED_BUSINESS ,
) :
return PlanTierChangeType . UPGRADE
2024-03-15 04:20:47 +01:00
elif (
current_plan_tier == CustomerPlan . TIER_SELF_HOSTED_BASIC
and new_plan_tier == CustomerPlan . TIER_SELF_HOSTED_LEGACY
) :
return PlanTierChangeType . DOWNGRADE
elif (
current_plan_tier == CustomerPlan . TIER_SELF_HOSTED_BUSINESS
and new_plan_tier == CustomerPlan . TIER_SELF_HOSTED_LEGACY
) :
return PlanTierChangeType . DOWNGRADE
2023-12-04 14:17:28 +01:00
else :
2023-12-18 23:57:32 +01:00
assert current_plan_tier == CustomerPlan . TIER_SELF_HOSTED_BUSINESS
assert new_plan_tier == CustomerPlan . TIER_SELF_HOSTED_BASIC
2023-12-04 14:17:28 +01:00
return PlanTierChangeType . DOWNGRADE
2023-11-23 07:29:03 +01:00
2023-11-27 11:07:03 +01:00
@override
2023-12-13 05:49:15 +01:00
def has_billing_access ( self ) - > bool : # nocoverage
2023-11-27 11:07:03 +01:00
# We don't currently have a way to authenticate a remote
# session that isn't authorized for billing access.
return True
2023-12-01 10:43:04 +01:00
PAID_PLANS = [
2023-12-18 23:57:32 +01:00
RemoteRealm . PLAN_TYPE_BASIC ,
2023-12-01 10:43:04 +01:00
RemoteRealm . PLAN_TYPE_BUSINESS ,
RemoteRealm . PLAN_TYPE_ENTERPRISE ,
]
2023-11-27 13:08:43 +01:00
@override
2023-12-13 05:49:15 +01:00
def on_paid_plan ( self ) - > bool : # nocoverage
2023-12-01 10:43:04 +01:00
return self . remote_realm . plan_type in self . PAID_PLANS
2023-11-27 13:08:43 +01:00
2024-02-01 05:07:01 +01:00
@override
def org_name ( self ) - > str :
return self . remote_realm . host
2023-11-27 13:08:43 +01:00
@override
2024-07-12 02:30:17 +02:00
def add_sponsorship_info_to_context ( self , context : dict [ str , Any ] ) - > None :
2023-11-30 01:48:46 +01:00
context . update (
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 " )
) ,
2023-12-01 03:50:13 +01:00
key = sponsorship_org_type_key_helper ,
2023-11-30 01:48:46 +01:00
) ,
)
@override
def get_sponsorship_request_session_specific_context (
self ,
2023-12-13 05:49:15 +01:00
) - > SponsorshipRequestSessionSpecificContext : # nocoverage
2023-12-12 19:57:27 +01:00
assert self . remote_billing_user is not None
2023-11-30 01:48:46 +01:00
return SponsorshipRequestSessionSpecificContext (
realm_user = None ,
user_info = SponsorshipApplicantInfo (
2023-12-12 19:57:27 +01:00
name = self . remote_billing_user . full_name ,
2023-12-02 03:54:24 +01:00
email = self . get_email ( ) ,
2023-12-12 19:57:27 +01:00
# We don't have role data for the user.
2023-11-30 01:48:46 +01:00
role = " Remote realm administrator " ,
) ,
# TODO: Check if this works on support page.
realm_string_id = self . remote_realm . host ,
)
@override
2023-12-13 05:49:15 +01:00
def save_org_type_from_request_sponsorship_session ( self , org_type : int ) - > None : # nocoverage
2023-11-30 01:48:46 +01:00
if self . remote_realm . org_type != org_type :
self . remote_realm . org_type = org_type
self . remote_realm . save ( update_fields = [ " org_type " ] )
2023-11-27 13:08:43 +01:00
2023-12-12 09:02:17 +01:00
@override
2023-12-13 12:25:23 +01:00
def sync_license_ledger_if_needed ( self ) - > None :
2023-12-12 08:12:26 +01:00
last_ledger = self . get_last_ledger_for_automanaged_plan_if_exists ( )
if last_ledger is None :
2023-12-08 13:19:24 +01:00
return
# New audit logs since last_ledger for the plan was created.
new_audit_logs = (
RemoteRealmAuditLog . objects . filter (
remote_realm = self . remote_realm ,
event_time__gt = last_ledger . event_time ,
event_type__in = RemoteRealmAuditLog . SYNCED_BILLING_EVENTS ,
)
. exclude ( extra_data = { } )
. order_by ( " event_time " )
)
2023-12-12 08:12:26 +01:00
current_plan = last_ledger . plan
2023-12-08 13:19:24 +01:00
for audit_log in new_audit_logs :
2023-12-12 08:12:26 +01:00
end_of_cycle_plan = self . update_license_ledger_for_automanaged_plan (
current_plan , audit_log . event_time
)
if end_of_cycle_plan is None :
2023-12-13 12:25:23 +01:00
return # nocoverage
2023-12-12 08:12:26 +01:00
current_plan = end_of_cycle_plan
2023-12-08 13:19:24 +01:00
2023-11-09 20:40:42 +01:00
2023-12-13 05:49:15 +01:00
class RemoteServerBillingSession ( BillingSession ) :
2023-11-09 20:40:42 +01:00
""" Billing session for pre-8.0 servers that do not yet support
creating RemoteRealm objects . """
def __init__ (
2023-11-30 01:48:46 +01:00
self ,
remote_server : RemoteZulipServer ,
2024-07-12 02:30:23 +02:00
remote_billing_user : RemoteServerBillingUser | None = None ,
support_staff : UserProfile | None = None ,
2023-11-09 20:40:42 +01:00
) - > None :
self . remote_server = remote_server
2023-12-08 19:00:04 +01:00
self . remote_billing_user = remote_billing_user
2023-12-14 15:50:12 +01:00
self . support_staff = support_staff
2023-12-13 05:49:15 +01:00
if support_staff is not None : # nocoverage
2023-11-09 20:40:42 +01:00
assert support_staff . is_staff
self . support_session = True
else :
self . support_session = False
2023-11-30 21:11:54 +01:00
@override
@property
2023-12-13 05:49:15 +01:00
def billing_entity_display_name ( self ) - > str : # nocoverage
2023-11-30 21:11:54 +01:00
return self . remote_server . hostname
2023-11-09 20:40:42 +01:00
@override
@property
2023-12-13 05:49:15 +01:00
def billing_session_url ( self ) - > str : # nocoverage
2023-12-01 06:44:59 +01:00
return f " { settings . EXTERNAL_URI_SCHEME } { settings . SELF_HOSTING_MANAGEMENT_SUBDOMAIN } . { settings . EXTERNAL_HOST } /server/ { self . remote_server . uuid } "
2023-11-09 20:40:42 +01:00
2023-12-01 04:18:58 +01:00
@override
@property
def billing_base_url ( self ) - > str :
2023-12-01 06:44:59 +01:00
return f " /server/ { self . remote_server . uuid } "
2023-12-01 04:18:58 +01:00
2023-11-30 01:48:46 +01:00
@override
2023-12-13 05:49:15 +01:00
def support_url ( self ) - > str : # nocoverage
2024-02-28 21:25:12 +01:00
return build_support_url ( " remote_servers_support " , str ( self . remote_server . uuid ) )
2023-11-30 01:48:46 +01:00
2023-11-09 20:40:42 +01:00
@override
2024-07-12 02:30:23 +02:00
def get_customer ( self ) - > Customer | None :
2023-11-09 20:40:42 +01:00
return get_customer_by_remote_server ( self . remote_server )
2023-12-02 03:54:24 +01:00
@override
def get_email ( self ) - > str :
2023-12-12 19:57:27 +01:00
assert self . remote_billing_user is not None
return self . remote_billing_user . email
2023-12-02 03:54:24 +01:00
2023-11-09 20:40:42 +01:00
@override
2023-12-19 12:24:15 +01:00
def current_count_for_billed_licenses ( self , event_time : datetime = timezone_now ( ) ) - > int :
2023-12-06 19:25:49 +01:00
if has_stale_audit_log ( self . remote_server ) :
raise MissingDataError
2023-12-08 13:19:24 +01:00
remote_server_counts = get_remote_server_guest_and_non_guest_count (
self . remote_server . id , event_time
)
2023-12-07 01:39:05 +01:00
return remote_server_counts . non_guest_user_count + remote_server_counts . guest_user_count
2023-11-09 20:40:42 +01:00
2023-12-13 05:49:15 +01:00
def missing_data_error_page ( self , request : HttpRequest ) - > HttpResponse : # nocoverage
2023-12-07 15:27:39 +01:00
# The remedy for a RemoteZulipServer login is usually
# upgrading to Zulip 8.0 or enabling SUBMIT_USAGE_STATISTICS.
missing_data_context = {
" remote_realm_session " : False ,
" supports_remote_realms " : self . remote_server . last_api_feature_level is not None ,
}
return render (
2024-01-29 15:46:18 +01:00
request ,
" corporate/billing/server_not_uploading_data.html " ,
context = missing_data_context ,
2023-12-07 15:27:39 +01:00
)
2023-11-09 20:40:42 +01:00
@override
2024-08-29 20:48:40 +02:00
def get_audit_log_event ( self , event_type : BillingSessionEventType ) - > int :
if event_type is BillingSessionEventType . STRIPE_CUSTOMER_CREATED :
2023-11-09 20:40:42 +01:00
return RemoteZulipServerAuditLog . STRIPE_CUSTOMER_CREATED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . STRIPE_CARD_CHANGED :
2023-11-09 20:40:42 +01:00
return RemoteZulipServerAuditLog . STRIPE_CARD_CHANGED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . CUSTOMER_PLAN_CREATED :
2023-11-09 20:40:42 +01:00
return RemoteZulipServerAuditLog . CUSTOMER_PLAN_CREATED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . DISCOUNT_CHANGED :
2023-12-19 12:24:15 +01:00
return RemoteZulipServerAuditLog . REMOTE_SERVER_DISCOUNT_CHANGED # nocoverage
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . CUSTOMER_PROPERTY_CHANGED :
2024-01-12 17:38:55 +01:00
return RemoteZulipServerAuditLog . CUSTOMER_PROPERTY_CHANGED # nocoverage
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . SPONSORSHIP_APPROVED :
2023-11-09 20:40:42 +01:00
return RemoteZulipServerAuditLog . REMOTE_SERVER_SPONSORSHIP_APPROVED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . SPONSORSHIP_PENDING_STATUS_CHANGED :
2023-11-09 20:40:42 +01:00
return RemoteZulipServerAuditLog . REMOTE_SERVER_SPONSORSHIP_PENDING_STATUS_CHANGED
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . BILLING_MODALITY_CHANGED :
2023-12-19 12:24:15 +01:00
return RemoteZulipServerAuditLog . REMOTE_SERVER_BILLING_MODALITY_CHANGED # nocoverage
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . CUSTOMER_PLAN_PROPERTY_CHANGED :
2024-01-12 17:38:55 +01:00
return RemoteZulipServerAuditLog . CUSTOMER_PLAN_PROPERTY_CHANGED # nocoverage
2024-08-29 20:48:40 +02:00
elif event_type is BillingSessionEventType . BILLING_ENTITY_PLAN_TYPE_CHANGED :
2023-12-04 23:20:49 +01:00
return RemoteZulipServerAuditLog . REMOTE_SERVER_PLAN_TYPE_CHANGED
2023-12-19 12:24:15 +01:00
elif (
2024-08-29 20:48:40 +02:00
event_type is BillingSessionEventType . CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN
2023-12-19 12:24:15 +01:00
) : # nocoverage
2023-12-08 02:59:35 +01:00
return RemoteZulipServerAuditLog . CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN
2023-12-19 12:24:15 +01:00
elif (
2024-08-29 20:48:40 +02:00
event_type is BillingSessionEventType . CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN
2023-12-19 12:24:15 +01:00
) : # nocoverage
2023-12-08 02:59:35 +01:00
return RemoteZulipServerAuditLog . CUSTOMER_SWITCHED_FROM_ANNUAL_TO_MONTHLY_PLAN
2023-12-19 12:24:15 +01:00
else : # nocoverage
2023-11-09 20:40:42 +01:00
raise BillingSessionAuditLogEventError ( event_type )
@override
def write_to_audit_log (
self ,
2024-08-29 20:48:40 +02:00
event_type : BillingSessionEventType ,
2023-11-09 20:40:42 +01:00
event_time : datetime ,
* ,
2023-12-24 15:56:33 +01:00
background_update : bool = False ,
2024-07-12 02:30:23 +02:00
extra_data : dict [ str , Any ] | None = None ,
2023-12-19 12:24:15 +01:00
) - > None :
2023-11-09 20:40:42 +01:00
audit_log_event = self . get_audit_log_event ( event_type )
2023-11-30 12:20:45 +01:00
log_data = {
" server " : self . remote_server ,
" event_type " : audit_log_event ,
" event_time " : event_time ,
}
2023-12-24 15:56:33 +01:00
if not background_update :
log_data . update (
{
# At most one of these should be set, but we may
# not want an assert for that yet:
" acting_support_user " : self . support_staff ,
" acting_remote_user " : self . remote_billing_user ,
}
)
2023-11-09 20:40:42 +01:00
if extra_data :
2023-11-30 12:20:45 +01:00
log_data [ " extra_data " ] = extra_data
RemoteZulipServerAuditLog . objects . create ( * * log_data )
2023-11-09 20:40:42 +01:00
@override
2023-12-19 12:24:15 +01:00
def get_data_for_stripe_customer ( self ) - > StripeCustomerData :
2023-11-09 20:40:42 +01:00
# Support requests do not set any stripe billing information.
assert self . support_session is False
2024-07-12 02:30:17 +02:00
metadata : dict [ str , Any ] = { }
2023-11-09 20:40:42 +01:00
metadata [ " remote_server_uuid " ] = self . remote_server . uuid
metadata [ " remote_server_str " ] = str ( self . remote_server )
realm_stripe_customer_data = StripeCustomerData (
description = str ( self . remote_server ) ,
2023-12-02 03:54:24 +01:00
email = self . get_email ( ) ,
2023-11-09 20:40:42 +01:00
metadata = metadata ,
)
return realm_stripe_customer_data
@override
2024-02-10 07:47:32 +01:00
def update_data_for_checkout_session_and_invoice_payment (
2024-07-12 02:30:17 +02:00
self , metadata : dict [ str , Any ]
) - > dict [ str , Any ] :
2023-12-13 10:41:23 +01:00
assert self . remote_billing_user is not None
2023-11-09 20:40:42 +01:00
updated_metadata = dict (
2023-12-13 10:41:23 +01:00
remote_server_user_id = self . remote_billing_user . id ,
remote_server_user_email = self . get_email ( ) ,
remote_server_host = self . remote_server . hostname ,
2023-11-09 20:40:42 +01:00
* * metadata ,
)
return updated_metadata
@override
def update_or_create_customer (
2024-07-12 02:30:23 +02:00
self , stripe_customer_id : str | None = None , * , defaults : dict [ str , Any ] | None = None
2023-12-19 12:24:15 +01:00
) - > Customer :
2023-11-09 20:40:42 +01:00
if stripe_customer_id is not None :
# Support requests do not set any stripe billing information.
assert self . support_session is False
customer , created = Customer . objects . update_or_create (
remote_server = self . remote_server ,
defaults = { " stripe_customer_id " : stripe_customer_id } ,
)
else :
customer , created = Customer . objects . update_or_create (
remote_server = self . remote_server , defaults = defaults
)
2023-12-20 07:24:21 +01:00
2024-05-06 06:12:15 +02:00
if (
created
and not customer . annual_discounted_price
and not customer . monthly_discounted_price
) :
2023-12-20 07:24:21 +01:00
customer . flat_discounted_months = 12
customer . save ( update_fields = [ " flat_discounted_months " ] )
return customer
2023-11-09 20:40:42 +01:00
@override
2023-12-18 21:09:33 +01:00
@transaction.atomic
2023-12-24 15:56:33 +01:00
def do_change_plan_type (
2024-07-12 02:30:23 +02:00
self , * , tier : int | None , is_sponsored : bool = False , background_update : bool = False
2023-12-24 15:56:33 +01:00
) - > None :
2023-11-09 20:40:42 +01:00
# This function needs to translate between the different
# formats of CustomerPlan.tier and RealmZulipServer.plan_type.
if is_sponsored :
plan_type = RemoteZulipServer . PLAN_TYPE_COMMUNITY
2023-12-18 21:09:33 +01:00
self . add_customer_to_community_plan ( )
2023-12-18 23:57:32 +01:00
elif tier == CustomerPlan . TIER_SELF_HOSTED_BASIC :
plan_type = RemoteZulipServer . PLAN_TYPE_BASIC
2023-12-02 04:21:50 +01:00
elif tier == CustomerPlan . TIER_SELF_HOSTED_BUSINESS :
2023-11-09 20:40:42 +01:00
plan_type = RemoteZulipServer . PLAN_TYPE_BUSINESS
2023-12-14 01:16:03 +01:00
elif tier == CustomerPlan . TIER_SELF_HOSTED_LEGACY :
plan_type = RemoteZulipServer . PLAN_TYPE_SELF_MANAGED_LEGACY
2023-11-09 20:40:42 +01:00
else :
raise AssertionError ( " Unexpected tier " )
2023-12-12 19:57:27 +01:00
old_plan_type = self . remote_server . plan_type
2023-11-09 20:40:42 +01:00
self . remote_server . plan_type = plan_type
self . remote_server . save ( update_fields = [ " plan_type " ] )
2023-12-12 19:57:27 +01:00
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . BILLING_ENTITY_PLAN_TYPE_CHANGED ,
2023-12-12 19:57:27 +01:00
event_time = timezone_now ( ) ,
extra_data = { " old_value " : old_plan_type , " new_value " : plan_type } ,
2023-12-24 15:56:33 +01:00
background_update = background_update ,
2023-12-12 19:57:27 +01:00
)
2023-11-09 20:40:42 +01:00
@override
2023-12-13 05:49:15 +01:00
def approve_sponsorship ( self ) - > str : # nocoverage
2023-12-02 18:21:04 +01:00
# Sponsorship approval is only a support admin action.
assert self . support_session
2024-01-05 01:50:20 +01:00
# Check no realm has a current plan, which would mean
# approving this sponsorship would violate our invariant that
# we never have active plans for both a remote realm and its
# remote server.
realm_plans = CustomerPlan . objects . filter (
customer__remote_realm__server = self . remote_server
) . exclude ( status = CustomerPlan . ENDED )
if realm_plans . exists ( ) :
return " Cannot approve server-level Community plan while some realms active plans. "
2023-12-02 18:21:04 +01:00
customer = self . get_customer ( )
2023-12-13 10:21:48 +01:00
if customer is not None :
error_message = self . check_customer_not_on_paid_plan ( customer )
if error_message != " " :
2024-01-03 20:22:49 +01:00
raise SupportRequestError ( error_message )
2023-12-13 10:21:48 +01:00
2023-12-14 05:35:11 +01:00
if self . remote_server . plan_type == RemoteZulipServer . PLAN_TYPE_SELF_MANAGED_LEGACY :
plan = get_current_plan_by_customer ( customer )
# Ideally we should have always have a plan here but since this is support page, we can be lenient about it.
if plan is not None :
assert self . get_next_plan ( plan ) is None
assert plan . tier == CustomerPlan . TIER_SELF_HOSTED_LEGACY
plan . status = CustomerPlan . ENDED
plan . save ( update_fields = [ " status " ] )
2023-12-13 10:21:48 +01:00
self . do_change_plan_type ( tier = None , is_sponsored = True )
2023-12-02 18:21:04 +01:00
if customer is not None and customer . sponsorship_pending :
customer . sponsorship_pending = False
customer . save ( update_fields = [ " sponsorship_pending " ] )
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . SPONSORSHIP_APPROVED , event_time = timezone_now ( )
2023-12-02 18:21:04 +01:00
)
2023-12-12 19:57:27 +01:00
billing_emails = list (
RemoteServerBillingUser . objects . filter ( remote_server = self . remote_server ) . values_list (
" email " , flat = True
)
)
2024-02-16 18:16:39 +01:00
if len ( billing_emails ) > 0 :
send_email (
" zerver/emails/sponsorship_approved_community_plan " ,
to_emails = billing_emails ,
from_address = BILLING_SUPPORT_EMAIL ,
context = {
" billing_entity " : self . billing_entity_display_name ,
" plans_link " : " https://zulip.com/plans/#self-hosted " ,
" link_to_zulip " : " https://zulip.com/help/linking-to-zulip-website " ,
} ,
)
emailed_string = " Emailed existing billing users. "
else :
emailed_string = " No billing users exist to email. "
return f " Sponsorship approved for { self . billing_entity_display_name } ; " + emailed_string
2023-11-09 20:40:42 +01:00
@override
2023-12-24 15:56:33 +01:00
def process_downgrade (
self , plan : CustomerPlan , background_update : bool = False
) - > None : # nocoverage
2023-12-04 23:20:49 +01:00
with transaction . atomic ( ) :
old_plan_type = self . remote_server . plan_type
2023-12-14 00:17:55 +01:00
new_plan_type = RemoteZulipServer . PLAN_TYPE_SELF_MANAGED
2023-12-04 23:20:49 +01:00
self . remote_server . plan_type = new_plan_type
self . remote_server . save ( update_fields = [ " plan_type " ] )
self . write_to_audit_log (
2024-08-29 20:48:40 +02:00
event_type = BillingSessionEventType . BILLING_ENTITY_PLAN_TYPE_CHANGED ,
2023-12-04 23:20:49 +01:00
event_time = timezone_now ( ) ,
extra_data = { " old_value " : old_plan_type , " new_value " : new_plan_type } ,
2023-12-24 15:56:33 +01:00
background_update = background_update ,
2023-12-04 23:20:49 +01:00
)
2023-11-09 20:40:42 +01:00
plan . status = CustomerPlan . ENDED
plan . save ( update_fields = [ " status " ] )
2023-11-27 13:25:11 +01:00
@override
def is_sponsored ( self ) - > bool :
return self . remote_server . plan_type == self . remote_server . PLAN_TYPE_COMMUNITY
2023-12-01 03:51:05 +01:00
@override
2024-07-12 02:30:17 +02:00
def get_metadata_for_stripe_update_card ( self ) - > dict [ str , str ] : # nocoverage
2023-12-12 19:57:27 +01:00
assert self . remote_billing_user is not None
return { " type " : " card_update " , " remote_server_user_id " : str ( self . remote_billing_user . id ) }
2023-12-01 03:51:05 +01:00
2023-11-20 08:40:09 +01:00
@override
2023-11-24 07:29:06 +01:00
def get_upgrade_page_session_type_specific_context (
self ,
) - > UpgradePageSessionTypeSpecificContext :
return UpgradePageSessionTypeSpecificContext (
customer_name = self . remote_server . hostname ,
2023-12-02 03:54:24 +01:00
email = self . get_email ( ) ,
2023-11-24 07:29:06 +01:00
is_demo_organization = False ,
demo_organization_scheduled_deletion_date = None ,
is_self_hosting = True ,
)
2023-11-20 08:40:09 +01:00
2024-01-10 17:20:08 +01:00
@override
def check_plan_tier_is_billable ( self , plan_tier : int ) - > bool : # nocoverage
implemented_plan_tiers = [
CustomerPlan . TIER_SELF_HOSTED_BASIC ,
CustomerPlan . TIER_SELF_HOSTED_BUSINESS ,
]
if plan_tier in implemented_plan_tiers :
return True
return False
2023-11-23 07:29:03 +01:00
@override
2023-11-30 17:11:41 +01:00
def get_type_of_plan_tier_change (
self , current_plan_tier : int , new_plan_tier : int
2023-12-13 05:49:15 +01:00
) - > PlanTierChangeType : # nocoverage
2023-12-04 14:17:28 +01:00
valid_plan_tiers = [
CustomerPlan . TIER_SELF_HOSTED_LEGACY ,
2023-12-18 23:57:32 +01:00
CustomerPlan . TIER_SELF_HOSTED_BASIC ,
2023-12-04 14:17:28 +01:00
CustomerPlan . TIER_SELF_HOSTED_BUSINESS ,
]
if (
current_plan_tier not in valid_plan_tiers
or new_plan_tier not in valid_plan_tiers
or current_plan_tier == new_plan_tier
) :
return PlanTierChangeType . INVALID
if current_plan_tier == CustomerPlan . TIER_SELF_HOSTED_LEGACY and new_plan_tier in (
2023-12-18 23:57:32 +01:00
CustomerPlan . TIER_SELF_HOSTED_BASIC ,
2023-12-04 14:17:28 +01:00
CustomerPlan . TIER_SELF_HOSTED_BUSINESS ,
) :
return PlanTierChangeType . UPGRADE
elif (
2023-12-18 23:57:32 +01:00
current_plan_tier == CustomerPlan . TIER_SELF_HOSTED_BASIC
and new_plan_tier == CustomerPlan . TIER_SELF_HOSTED_BUSINESS
2023-12-04 14:17:28 +01:00
) :
return PlanTierChangeType . UPGRADE
2023-12-18 23:57:32 +01:00
elif (
current_plan_tier == CustomerPlan . TIER_SELF_HOSTED_BASIC
and new_plan_tier == CustomerPlan . TIER_SELF_HOSTED_LEGACY
) :
return PlanTierChangeType . DOWNGRADE
2023-12-04 14:17:28 +01:00
elif (
current_plan_tier == CustomerPlan . TIER_SELF_HOSTED_BUSINESS
and new_plan_tier == CustomerPlan . TIER_SELF_HOSTED_LEGACY
) :
return PlanTierChangeType . DOWNGRADE
else :
2023-12-18 23:57:32 +01:00
assert current_plan_tier == CustomerPlan . TIER_SELF_HOSTED_BUSINESS
assert new_plan_tier == CustomerPlan . TIER_SELF_HOSTED_BASIC
2023-12-04 14:17:28 +01:00
return PlanTierChangeType . DOWNGRADE
2023-11-23 07:29:03 +01:00
2023-11-27 11:07:03 +01:00
@override
def has_billing_access ( self ) - > bool :
# We don't currently have a way to authenticate a remote
# session that isn't authorized for billing access.
return True
2023-12-01 10:43:04 +01:00
PAID_PLANS = [
2023-12-18 23:57:32 +01:00
RemoteZulipServer . PLAN_TYPE_BASIC ,
2023-12-01 10:43:04 +01:00
RemoteZulipServer . PLAN_TYPE_BUSINESS ,
RemoteZulipServer . PLAN_TYPE_ENTERPRISE ,
]
2023-11-27 13:08:43 +01:00
@override
2023-12-13 05:49:15 +01:00
def on_paid_plan ( self ) - > bool : # nocoverage
2023-12-01 10:43:04 +01:00
return self . remote_server . plan_type in self . PAID_PLANS
2023-11-27 13:08:43 +01:00
2024-02-01 05:07:01 +01:00
@override
def org_name ( self ) - > str :
return self . remote_server . hostname
2023-11-27 13:08:43 +01:00
@override
2024-07-12 02:30:17 +02:00
def add_sponsorship_info_to_context ( self , context : dict [ str , Any ] ) - > None : # nocoverage
2023-11-30 01:48:46 +01:00
context . update (
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 " )
) ,
2023-12-01 03:50:13 +01:00
key = sponsorship_org_type_key_helper ,
2023-11-30 01:48:46 +01:00
) ,
)
@override
def get_sponsorship_request_session_specific_context (
self ,
2023-12-13 05:49:15 +01:00
) - > SponsorshipRequestSessionSpecificContext : # nocoverage
2023-12-12 19:57:27 +01:00
assert self . remote_billing_user is not None
2023-11-30 01:48:46 +01:00
return SponsorshipRequestSessionSpecificContext (
realm_user = None ,
user_info = SponsorshipApplicantInfo (
2023-12-12 19:57:27 +01:00
name = self . remote_billing_user . full_name ,
2023-12-02 03:54:24 +01:00
email = self . get_email ( ) ,
2023-12-12 19:57:27 +01:00
# We don't have role data for the user.
2023-11-30 01:48:46 +01:00
role = " Remote server administrator " ,
) ,
# TODO: Check if this works on support page.
realm_string_id = self . remote_server . hostname ,
)
@override
2023-12-13 05:49:15 +01:00
def save_org_type_from_request_sponsorship_session ( self , org_type : int ) - > None : # nocoverage
2023-11-30 01:48:46 +01:00
if self . remote_server . org_type != org_type :
self . remote_server . org_type = org_type
self . remote_server . save ( update_fields = [ " org_type " ] )
2023-11-27 13:08:43 +01:00
2023-12-12 09:02:17 +01:00
@override
2023-12-19 12:24:15 +01:00
def sync_license_ledger_if_needed ( self ) - > None :
2023-12-12 10:24:03 +01:00
last_ledger = self . get_last_ledger_for_automanaged_plan_if_exists ( )
if last_ledger is None :
return
# New audit logs since last_ledger for the plan was created.
new_audit_logs = (
RemoteRealmAuditLog . objects . filter (
server = self . remote_server ,
event_time__gt = last_ledger . event_time ,
event_type__in = RemoteRealmAuditLog . SYNCED_BILLING_EVENTS ,
)
. exclude ( extra_data = { } )
. order_by ( " event_time " )
)
current_plan = last_ledger . plan
for audit_log in new_audit_logs :
end_of_cycle_plan = self . update_license_ledger_for_automanaged_plan (
current_plan , audit_log . event_time
)
2023-12-19 12:24:15 +01:00
if end_of_cycle_plan is None : # nocoverage
2023-12-12 10:24:03 +01:00
return
current_plan = end_of_cycle_plan
2023-12-12 09:02:17 +01:00
2023-11-09 20:40:42 +01:00
2021-08-29 15:33:29 +02:00
def stripe_customer_has_credit_card_as_default_payment_method (
stripe_customer : stripe . Customer ,
) - > bool :
2023-11-14 21:48:14 +01:00
assert stripe_customer . invoice_settings is not None
2021-08-29 15:33:29 +02:00
if not stripe_customer . invoice_settings . default_payment_method :
2020-10-14 12:17:03 +02:00
return False
2023-11-14 21:48:14 +01:00
assert isinstance ( stripe_customer . invoice_settings . default_payment_method , stripe . PaymentMethod )
2021-08-29 15:33:29 +02:00
return stripe_customer . invoice_settings . default_payment_method . type == " card "
2020-10-14 12:17:03 +02:00
2021-08-29 15:33:29 +02:00
def customer_has_credit_card_as_default_payment_method ( customer : Customer ) - > bool :
2020-10-14 12:17:03 +02:00
if not customer . stripe_customer_id :
return False
stripe_customer = stripe_get_customer ( customer . stripe_customer_id )
2021-08-29 15:33:29 +02:00
return stripe_customer_has_credit_card_as_default_payment_method ( stripe_customer )
2020-10-14 12:17:03 +02:00
2021-02-12 08:19:30 +01:00
def get_price_per_license (
2024-07-12 02:30:23 +02:00
tier : int , billing_schedule : int , customer : Customer | None = None
2021-02-12 08:19:30 +01:00
) - > int :
2024-05-06 06:12:15 +02:00
if customer is not None :
price_per_license = customer . get_discounted_price_for_plan ( tier , billing_schedule )
if price_per_license :
# We already have a set discounted price for the current tier.
return price_per_license
2024-07-12 02:30:17 +02:00
price_map : dict [ int , dict [ str , int ] ] = {
2023-12-01 12:29:24 +01:00
CustomerPlan . TIER_CLOUD_STANDARD : { " Annual " : 8000 , " Monthly " : 800 } ,
2024-04-16 10:19:00 +02:00
CustomerPlan . TIER_CLOUD_PLUS : { " Annual " : 12000 , " Monthly " : 1200 } ,
2023-12-18 23:57:32 +01:00
CustomerPlan . TIER_SELF_HOSTED_BASIC : { " Annual " : 4200 , " Monthly " : 350 } ,
2023-12-01 12:47:09 +01:00
CustomerPlan . TIER_SELF_HOSTED_BUSINESS : { " Annual " : 8000 , " Monthly " : 800 } ,
2023-12-09 08:42:10 +01:00
# To help with processing discount request on support page.
CustomerPlan . TIER_SELF_HOSTED_LEGACY : { " Annual " : 0 , " Monthly " : 0 } ,
2023-12-01 12:29:24 +01:00
}
2021-09-15 13:54:56 +02:00
2023-12-01 12:29:24 +01:00
try :
price_per_license = price_map [ tier ] [ CustomerPlan . BILLING_SCHEDULES [ billing_schedule ] ]
except KeyError :
if tier not in price_map :
raise InvalidTierError ( tier )
2021-09-15 13:54:56 +02:00
else : # nocoverage
2022-11-17 09:30:48 +01:00
raise InvalidBillingScheduleError ( billing_schedule )
2021-09-15 13:54:56 +02:00
2020-12-04 12:56:58 +01:00
return price_per_license
2021-02-12 08:19:30 +01:00
2024-05-06 06:12:15 +02:00
def get_price_per_license_and_discount (
2024-07-12 02:30:23 +02:00
tier : int , billing_schedule : int , customer : Customer | None
) - > tuple [ int , str | None ] :
2024-05-06 06:12:15 +02:00
original_price_per_license = get_price_per_license ( tier , billing_schedule )
if customer is None :
return original_price_per_license , None
price_per_license = get_price_per_license ( tier , billing_schedule , customer )
if price_per_license == original_price_per_license :
return price_per_license , None
discount = format_discount_percentage (
Decimal ( ( original_price_per_license - price_per_license ) / original_price_per_license * 100 )
)
return price_per_license , discount
2018-12-15 09:33:25 +01:00
def compute_plan_parameters (
2021-09-15 13:10:27 +02:00
tier : int ,
2021-02-12 08:19:30 +01:00
billing_schedule : int ,
2024-07-12 02:30:23 +02:00
customer : Customer | None ,
2021-02-12 08:19:30 +01:00
free_trial : bool = False ,
2024-07-12 02:30:23 +02:00
billing_cycle_anchor : datetime | None = None ,
2023-12-10 05:13:00 +01:00
is_self_hosted_billing : bool = False ,
2024-01-22 11:20:05 +01:00
should_schedule_upgrade_for_legacy_remote_server : bool = False ,
2024-07-12 02:30:17 +02:00
) - > tuple [ datetime , datetime , datetime , int ] :
2018-12-15 09:33:25 +01:00
# Everything in Stripe is stored as timestamps with 1 second resolution,
# so standardize on 1 second resolution.
2022-02-08 00:13:33 +01:00
# TODO talk about leap seconds?
2023-12-04 14:20:08 +01:00
if billing_cycle_anchor is None :
billing_cycle_anchor = timezone_now ( ) . replace ( microsecond = 0 )
2023-11-30 07:55:53 +01:00
if billing_schedule == CustomerPlan . BILLING_SCHEDULE_ANNUAL :
2018-12-15 09:33:25 +01:00
period_end = add_months ( billing_cycle_anchor , 12 )
2023-11-30 07:55:53 +01:00
elif billing_schedule == CustomerPlan . BILLING_SCHEDULE_MONTHLY :
2018-12-15 09:33:25 +01:00
period_end = add_months ( billing_cycle_anchor , 1 )
2020-12-04 12:56:58 +01:00
else : # nocoverage
2022-11-17 09:30:48 +01:00
raise InvalidBillingScheduleError ( billing_schedule )
2020-12-04 12:56:58 +01:00
2024-05-06 06:12:15 +02:00
price_per_license = get_price_per_license ( tier , billing_schedule , customer )
2020-12-04 12:56:58 +01:00
2024-03-05 06:06:57 +01:00
# `next_invoice_date` is the date when we check if there are any invoices that need to be generated.
# It is always the next month regardless of the billing schedule / billing modality.
next_invoice_date = add_months ( billing_cycle_anchor , 1 )
2020-04-23 20:10:15 +02:00
if free_trial :
2021-07-25 16:31:12 +02:00
period_end = billing_cycle_anchor + timedelta (
2024-01-07 05:58:39 +01:00
days = assert_is_not_none ( get_free_trial_days ( is_self_hosted_billing , tier ) )
2021-07-25 16:31:12 +02:00
)
2020-04-23 20:10:15 +02:00
next_invoice_date = period_end
2024-01-22 11:20:05 +01:00
if should_schedule_upgrade_for_legacy_remote_server :
next_invoice_date = billing_cycle_anchor
2019-01-28 14:18:21 +01:00
return billing_cycle_anchor , next_invoice_date , period_end , price_per_license
2018-12-15 09:33:25 +01:00
2021-02-12 08:19:30 +01:00
2024-01-07 05:58:39 +01:00
def get_free_trial_days (
2024-07-12 02:30:23 +02:00
is_self_hosted_billing : bool = False , tier : int | None = None
) - > int | None :
2023-12-10 05:13:00 +01:00
if is_self_hosted_billing :
2024-01-07 05:58:39 +01:00
# Free trial is only available for self-hosted basic plan.
if tier is not None and tier != CustomerPlan . TIER_SELF_HOSTED_BASIC :
return None
2023-12-10 05:13:00 +01:00
return settings . SELF_HOSTING_FREE_TRIAL_DAYS
2023-12-09 18:31:27 +01:00
return settings . CLOUD_FREE_TRIAL_DAYS
2023-12-09 18:27:09 +01:00
2024-07-12 02:30:23 +02:00
def is_free_trial_offer_enabled ( is_self_hosted_billing : bool , tier : int | None = None ) - > bool :
2024-01-07 05:58:39 +01:00
return get_free_trial_days ( is_self_hosted_billing , tier ) not in ( None , 0 )
2020-10-14 09:44:01 +02:00
2023-10-30 22:29:22 +01:00
def ensure_customer_does_not_have_active_plan ( customer : Customer ) - > None :
if get_current_plan_by_customer ( customer ) is not None :
2021-08-29 15:33:29 +02:00
# Unlikely race condition from two people upgrading (clicking "Make payment")
# at exactly the same time. Doesn't fully resolve the race condition, but having
# a check here reduces the likelihood.
billing_logger . warning (
" Upgrade of %s failed because of existing active plan. " ,
2023-10-30 22:29:22 +01:00
str ( customer ) ,
2021-08-29 15:33:29 +02:00
)
2023-02-04 02:07:20 +01:00
raise UpgradeWithExistingPlanError
2021-08-29 15:33:29 +02:00
2024-02-18 01:41:37 +01:00
@transaction.atomic
def do_reactivate_remote_server ( remote_server : RemoteZulipServer ) - > None :
"""
Utility function for reactivating deactivated registrations .
"""
if not remote_server . deactivated :
billing_logger . warning (
" Cannot reactivate remote server with ID %d , server is already active. " ,
remote_server . id ,
)
return
remote_server . deactivated = False
remote_server . save ( update_fields = [ " deactivated " ] )
RemoteZulipServerAuditLog . objects . create (
event_type = RealmAuditLog . REMOTE_SERVER_REACTIVATED ,
server = remote_server ,
event_time = timezone_now ( ) ,
)
2021-12-15 18:53:58 +01:00
@transaction.atomic
2023-12-13 02:44:55 +01:00
def do_deactivate_remote_server (
remote_server : RemoteZulipServer , billing_session : RemoteServerBillingSession
) - > None :
2021-12-15 18:53:58 +01:00
if remote_server . deactivated :
billing_logger . warning (
2023-02-04 01:42:19 +01:00
" Cannot deactivate remote server with ID %d , server has already been deactivated. " ,
remote_server . id ,
2021-12-15 18:53:58 +01:00
)
return
2023-12-13 02:44:55 +01:00
server_plans_to_consider = CustomerPlan . objects . filter (
customer__remote_server = remote_server
) . exclude ( status = CustomerPlan . ENDED )
realm_plans_to_consider = CustomerPlan . objects . filter (
customer__remote_realm__server = remote_server
) . exclude ( status = CustomerPlan . ENDED )
for possible_plan in list ( server_plans_to_consider ) + list ( realm_plans_to_consider ) :
if possible_plan . tier in [
CustomerPlan . TIER_SELF_HOSTED_BASE ,
CustomerPlan . TIER_SELF_HOSTED_LEGACY ,
CustomerPlan . TIER_SELF_HOSTED_COMMUNITY ,
] : # nocoverage
# No action required for free plans.
continue
if possible_plan . status in [
CustomerPlan . DOWNGRADE_AT_END_OF_FREE_TRIAL ,
CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE ,
] : # nocoverage
# No action required for plans scheduled to downgrade
# automatically.
continue
# This customer has some sort of paid plan; ask the customer
# to downgrade their paid plan so that they get the
# communication in that flow, and then they can come back and
# deactivate their server.
raise ServerDeactivateWithExistingPlanError # nocoverage
2021-12-15 18:53:58 +01:00
remote_server . deactivated = True
remote_server . save ( update_fields = [ " deactivated " ] )
RemoteZulipServerAuditLog . objects . create (
event_type = RealmAuditLog . REMOTE_SERVER_DEACTIVATED ,
server = remote_server ,
event_time = timezone_now ( ) ,
)
2021-09-21 21:21:03 +02:00
def get_plan_renewal_or_end_date ( plan : CustomerPlan , event_time : datetime ) - > datetime :
billing_period_end = start_of_next_billing_cycle ( plan , event_time )
if plan . end_date is not None and plan . end_date < billing_period_end :
return plan . end_date
return billing_period_end
2024-07-12 02:30:23 +02:00
def invoice_plans_as_needed ( event_time : datetime | None = None ) - > None :
2024-01-22 14:20:49 +01:00
if event_time is None :
2023-04-27 22:25:38 +02:00
event_time = timezone_now ( )
2024-01-22 11:20:05 +01:00
# For self hosted legacy plan with status SWITCH_PLAN_TIER_AT_PLAN_END, we need
# to invoice legacy plan followed by new plan on the same day, hence ordered by ID.
for plan in CustomerPlan . objects . filter ( next_invoice_date__lte = event_time ) . order_by ( " id " ) :
2024-07-12 02:30:23 +02:00
remote_server : RemoteZulipServer | None = None
2023-11-30 14:49:10 +01:00
if plan . customer . realm is not None :
2024-01-08 13:32:47 +01:00
billing_session : BillingSession = RealmBillingSession ( realm = plan . customer . realm )
2024-01-08 13:28:06 +01:00
elif plan . customer . remote_realm is not None :
remote_realm = plan . customer . remote_realm
remote_server = remote_realm . server
billing_session = RemoteRealmBillingSession ( remote_realm = remote_realm )
2024-01-08 13:32:47 +01:00
elif plan . customer . remote_server is not None :
remote_server = plan . customer . remote_server
billing_session = RemoteServerBillingSession ( remote_server = remote_server )
2024-01-08 13:28:06 +01:00
2024-03-07 09:55:00 +01:00
assert plan . next_invoice_date is not None # for mypy
2024-01-08 13:32:47 +01:00
if remote_server :
2024-02-15 08:00:20 +01:00
if (
plan . fixed_price is not None
and not plan . reminder_to_review_plan_email_sent
and plan . end_date is not None # for mypy
# The max gap between two months is 62 days. (1 Jul - 1 Sep)
and plan . end_date - plan . next_invoice_date < = timedelta ( days = 62 )
) :
context = {
" billing_entity " : billing_session . billing_entity_display_name ,
" end_date " : plan . end_date . strftime ( " % Y- % m- %d " ) ,
" support_url " : billing_session . support_url ( ) ,
2024-02-29 10:26:41 +01:00
" notice_reason " : " fixed_price_plan_ends_soon " ,
2024-02-15 08:00:20 +01:00
}
send_email (
2024-02-29 10:26:41 +01:00
" zerver/emails/internal_billing_notice " ,
2024-02-15 08:00:20 +01:00
to_emails = [ BILLING_SUPPORT_EMAIL ] ,
from_address = FromAddress . tokenized_no_reply_address ( ) ,
context = context ,
)
plan . reminder_to_review_plan_email_sent = True
plan . save ( update_fields = [ " reminder_to_review_plan_email_sent " ] )
2024-03-12 19:48:16 +01:00
free_plan_with_no_next_plan = (
not plan . is_a_paid_plan ( ) and plan . status == CustomerPlan . ACTIVE
)
2024-04-10 10:31:15 +02:00
free_trial_pay_by_invoice_plan = plan . is_free_trial ( ) and not plan . charge_automatically
2024-02-16 15:31:55 +01:00
last_audit_log_update = remote_server . last_audit_log_update
2024-03-28 12:59:15 +01:00
if not free_plan_with_no_next_plan and (
last_audit_log_update is None or plan . next_invoice_date > last_audit_log_update
) :
2024-01-08 13:28:06 +01:00
if (
2024-02-16 15:31:55 +01:00
last_audit_log_update is None
or plan . next_invoice_date - last_audit_log_update > = timedelta ( days = 1 )
) and not plan . invoice_overdue_email_sent :
last_audit_log_update_string = " Never uploaded "
if last_audit_log_update is not None :
last_audit_log_update_string = last_audit_log_update . strftime ( " % Y- % m- %d " )
2024-01-08 13:28:06 +01:00
context = {
2024-03-20 15:46:12 +01:00
" billing_entity " : billing_session . billing_entity_display_name ,
2024-01-08 13:28:06 +01:00
" support_url " : billing_session . support_url ( ) ,
2024-02-16 15:31:55 +01:00
" last_audit_log_update " : last_audit_log_update_string ,
2024-02-29 10:26:41 +01:00
" notice_reason " : " invoice_overdue " ,
2024-01-08 13:28:06 +01:00
}
send_email (
2024-02-29 10:26:41 +01:00
" zerver/emails/internal_billing_notice " ,
2024-01-08 13:28:06 +01:00
to_emails = [ BILLING_SUPPORT_EMAIL ] ,
from_address = FromAddress . tokenized_no_reply_address ( ) ,
context = context ,
)
plan . invoice_overdue_email_sent = True
plan . save ( update_fields = [ " invoice_overdue_email_sent " ] )
2024-04-10 10:31:15 +02:00
# We still process free trial plans so that we can directly downgrade them.
# Above emails can serve as a reminder to followup for additional feedback.
if not free_trial_pay_by_invoice_plan :
continue
2024-01-08 13:28:06 +01:00
2024-04-08 07:19:46 +02:00
while (
plan . next_invoice_date is not None # type: ignore[redundant-expr] # plan.next_invoice_date can be None after calling invoice_plan.
and plan . next_invoice_date < = event_time
) :
billing_session . invoice_plan ( plan , plan . next_invoice_date )
plan . refresh_from_db ( )
2019-01-28 22:57:29 +01:00
2021-02-12 08:19:30 +01:00
2020-11-11 14:09:30 +01:00
def is_realm_on_free_trial ( realm : Realm ) - > bool :
plan = get_current_plan_by_realm ( realm )
return plan is not None and plan . is_free_trial ( )
2019-04-08 05:16:35 +02:00
def do_change_plan_status ( plan : CustomerPlan , status : int ) - > None :
plan . status = status
2021-02-12 08:20:45 +01:00
plan . save ( update_fields = [ " status " ] )
2020-05-02 20:57:12 +02:00
billing_logger . info (
2021-02-12 08:20:45 +01:00
" Change plan status: Customer.id: %s , CustomerPlan.id: %s , status: %s " ,
2021-02-12 08:19:30 +01:00
plan . customer . id ,
plan . id ,
status ,
2020-05-02 20:57:12 +02:00
)
2019-04-08 05:16:35 +02:00
2021-02-12 08:19:30 +01:00
2021-06-11 10:10:17 +02:00
def get_all_invoices_for_customer ( customer : Customer ) - > Generator [ stripe . Invoice , None , None ] :
if customer . stripe_customer_id is None :
return
invoices = stripe . Invoice . list ( customer = customer . stripe_customer_id , limit = 100 )
while len ( invoices ) :
for invoice in invoices :
yield invoice
last_invoice = invoice
2024-04-30 19:21:01 +02:00
assert last_invoice . id is not None
2021-06-11 10:10:17 +02:00
invoices = stripe . Invoice . list (
2024-04-30 19:21:01 +02:00
customer = customer . stripe_customer_id , starting_after = last_invoice . id , limit = 100
2021-06-11 10:10:17 +02:00
)
2021-07-16 15:35:13 +02:00
def customer_has_last_n_invoices_open ( customer : Customer , n : int ) - > bool :
2021-07-16 17:25:32 +02:00
if customer . stripe_customer_id is None : # nocoverage
2021-07-16 15:35:13 +02:00
return False
open_invoice_count = 0
for invoice in stripe . Invoice . list ( customer = customer . stripe_customer_id , limit = n ) :
if invoice . status == " open " :
open_invoice_count + = 1
return open_invoice_count == n
2021-06-11 12:53:45 +02:00
def downgrade_small_realms_behind_on_payments_as_needed ( ) - > None :
2023-10-30 16:28:52 +01:00
customers = Customer . objects . all ( ) . exclude ( stripe_customer_id = None ) . exclude ( realm = None )
2021-06-11 12:53:45 +02:00
for customer in customers :
realm = customer . realm
2022-05-31 01:34:34 +02:00
assert realm is not None
2021-06-11 12:53:45 +02:00
# For larger realms, we generally want to talk to the customer
2021-07-16 17:13:49 +02:00
# before downgrading or cancelling invoices; so this logic only applies with 5.
2021-06-11 12:53:45 +02:00
if get_latest_seat_count ( realm ) > = 5 :
continue
2021-07-16 17:13:49 +02:00
if get_current_plan_by_customer ( customer ) is not None :
# Only customers with last 2 invoices open should be downgraded.
if not customer_has_last_n_invoices_open ( customer , 2 ) :
continue
2021-06-11 12:53:45 +02:00
2021-07-16 17:13:49 +02:00
# We've now decided to downgrade this customer and void all invoices, and the below will execute this.
2023-11-22 12:44:02 +01:00
billing_session = RealmBillingSession ( user = None , realm = realm )
billing_session . downgrade_now_without_creating_additional_invoices ( )
2023-12-01 17:48:41 +01:00
billing_session . void_all_open_invoices ( )
2024-07-12 02:30:23 +02:00
context : dict [ str , str | Realm ] = {
2024-05-06 15:27:22 +02:00
" upgrade_url " : f " { realm . url } { reverse ( ' upgrade_page ' ) } " ,
2021-07-16 17:13:49 +02:00
" realm " : realm ,
}
send_email_to_billing_admins_and_realm_owners (
" zerver/emails/realm_auto_downgraded " ,
realm ,
from_name = FromAddress . security_email_from_name ( language = realm . default_language ) ,
from_address = FromAddress . tokenized_no_reply_address ( ) ,
language = realm . default_language ,
context = context ,
)
else :
if customer_has_last_n_invoices_open ( customer , 1 ) :
2023-12-01 17:48:41 +01:00
# If a small realm, without an active plan, has
# the last invoice open, void the open invoices.
billing_session = RealmBillingSession ( user = None , realm = realm )
billing_session . void_all_open_invoices ( )
2023-12-11 09:32:44 +01:00
@dataclass
class PushNotificationsEnabledStatus :
can_push : bool
2024-07-12 02:30:23 +02:00
expected_end_timestamp : int | None
2023-12-11 09:32:44 +01:00
# Not sent to clients, just for debugging
message : str
2023-12-12 17:15:57 +01:00
MAX_USERS_WITHOUT_PLAN = 10
2023-12-11 09:32:44 +01:00
def get_push_status_for_remote_request (
2024-07-12 02:30:23 +02:00
remote_server : RemoteZulipServer , remote_realm : RemoteRealm | None
2023-12-11 09:32:44 +01:00
) - > PushNotificationsEnabledStatus :
# First, get the operative Customer object for this
2024-02-16 18:18:53 +01:00
# installation.
2023-12-11 09:32:44 +01:00
customer = None
2024-02-16 18:18:53 +01:00
current_plan = None
2024-07-12 02:30:23 +02:00
realm_billing_session : BillingSession | None = None
server_billing_session : RemoteServerBillingSession | None = None
2023-12-11 09:32:44 +01:00
if remote_realm is not None :
2024-02-21 20:44:46 +01:00
realm_billing_session = RemoteRealmBillingSession ( remote_realm )
if realm_billing_session . is_sponsored ( ) :
return PushNotificationsEnabledStatus (
can_push = True ,
expected_end_timestamp = None ,
message = " Community plan " ,
)
customer = realm_billing_session . get_customer ( )
2024-02-16 18:18:53 +01:00
if customer is not None :
current_plan = get_current_plan_by_customer ( customer )
2023-12-11 09:32:44 +01:00
2024-02-16 18:18:53 +01:00
# If there's a `RemoteRealm` customer with an active plan, that
# takes precedence, but look for a current plan on the server if
# there is a customer with only inactive/expired plans on the Realm.
if customer is None or current_plan is None :
2024-02-21 20:44:46 +01:00
server_billing_session = RemoteServerBillingSession ( remote_server )
if server_billing_session . is_sponsored ( ) :
return PushNotificationsEnabledStatus (
can_push = True ,
expected_end_timestamp = None ,
message = " Community plan " ,
)
customer = server_billing_session . get_customer ( )
2024-02-16 18:18:53 +01:00
if customer is not None :
current_plan = get_current_plan_by_customer ( customer )
2023-12-11 09:32:44 +01:00
2024-02-21 20:44:46 +01:00
if realm_billing_session is not None :
user_count_billing_session : BillingSession = realm_billing_session
else :
assert server_billing_session is not None
user_count_billing_session = server_billing_session
2023-12-17 06:59:16 +01:00
2024-07-12 02:30:23 +02:00
user_count : int | None = None
2024-01-05 15:02:55 +01:00
if current_plan is None :
try :
2024-02-21 20:44:46 +01:00
user_count = user_count_billing_session . current_count_for_billed_licenses ( )
2024-01-05 15:02:55 +01:00
except MissingDataError :
return PushNotificationsEnabledStatus (
can_push = False ,
expected_end_timestamp = None ,
message = " Missing data " ,
2023-12-11 09:32:44 +01:00
)
2024-01-05 15:02:55 +01:00
if user_count > MAX_USERS_WITHOUT_PLAN :
2023-12-11 09:32:44 +01:00
return PushNotificationsEnabledStatus (
2024-01-05 15:02:55 +01:00
can_push = False ,
expected_end_timestamp = None ,
2024-02-16 19:20:02 +01:00
message = " Push notifications access with 10+ users requires signing up for a plan. https://zulip.com/plans/ " ,
2023-12-11 09:32:44 +01:00
)
2024-01-05 15:02:55 +01:00
return PushNotificationsEnabledStatus (
can_push = True ,
expected_end_timestamp = None ,
message = " No plan few users " ,
)
if current_plan . status not in [
CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE ,
CustomerPlan . DOWNGRADE_AT_END_OF_FREE_TRIAL ,
] :
2023-12-11 09:32:44 +01:00
# Current plan, no expected end.
return PushNotificationsEnabledStatus (
can_push = True ,
expected_end_timestamp = None ,
message = " Active plan " ,
)
2023-12-12 17:15:57 +01:00
try :
2024-02-21 20:44:46 +01:00
user_count = user_count_billing_session . current_count_for_billed_licenses ( )
2023-12-12 17:15:57 +01:00
except MissingDataError :
2024-01-05 15:02:55 +01:00
user_count = None
2023-12-12 17:15:57 +01:00
2024-01-05 15:02:55 +01:00
if user_count is not None and user_count < = MAX_USERS_WITHOUT_PLAN :
# We have an expiring plan, but we know we have few enough
# users that once the plan expires, we will enter the "No plan
# few users" case, so don't notify users about the plan
# expiring via sending expected_end_timestamp.
2023-12-12 17:15:57 +01:00
return PushNotificationsEnabledStatus (
2024-01-05 15:02:55 +01:00
can_push = True ,
2023-12-12 17:15:57 +01:00
expected_end_timestamp = None ,
2024-01-05 15:02:55 +01:00
message = " Expiring plan few users " ,
2023-12-12 17:15:57 +01:00
)
2024-02-21 20:44:46 +01:00
# TODO: Move get_next_billing_cycle to be plan.get_next_billing_cycle
# to avoid this somewhat evil use of a possibly non-matching billing session.
2024-01-05 15:02:55 +01:00
expected_end_timestamp = datetime_to_timestamp (
2024-02-21 20:44:46 +01:00
user_count_billing_session . get_next_billing_cycle ( current_plan )
2024-01-05 15:02:55 +01:00
)
2023-12-11 09:32:44 +01:00
return PushNotificationsEnabledStatus (
can_push = True ,
2024-01-05 15:02:55 +01:00
expected_end_timestamp = expected_end_timestamp ,
message = " Scheduled end " ,
2023-12-11 09:32:44 +01:00
)