2020-06-11 00:54:34 +02:00
import json
2018-10-17 08:23:13 +02:00
import operator
2018-03-31 04:13:44 +02:00
import os
2018-08-08 16:35:33 +02:00
import re
2018-10-17 08:23:13 +02:00
import sys
2020-06-11 00:54:34 +02:00
from datetime import datetime , timedelta , timezone
from decimal import Decimal
from functools import wraps
2020-07-15 22:18:32 +02:00
from typing import Any , Callable , Dict , List , Mapping , Optional , Sequence , TypeVar , cast
2020-06-11 00:54:34 +02:00
from unittest . mock import Mock , patch
2018-03-31 04:13:44 +02:00
2020-08-07 01:09:47 +02:00
import orjson
2020-06-11 00:54:34 +02:00
import responses
import stripe
from django . conf import settings
2018-07-13 17:34:39 +02:00
from django . core import signing
2018-08-08 16:35:33 +02:00
from django . http import HttpResponse
2020-06-11 00:54:34 +02:00
from django . urls . resolvers import get_resolver
2020-03-24 14:14:03 +01:00
from django . utils . timezone import now as timezone_now
2018-07-13 17:34:39 +02:00
2020-06-11 00:54:34 +02:00
from corporate . lib . stripe import (
MAX_INVOICED_LICENSES ,
MIN_INVOICED_LICENSES ,
BillingError ,
2020-12-04 12:56:58 +01:00
InvalidBillingSchedule ,
2020-06-11 00:54:34 +02:00
StripeCardError ,
add_months ,
2020-12-04 11:16:33 +01:00
approve_sponsorship ,
2020-06-11 00:54:34 +02:00
attach_discount_to_realm ,
catch_stripe_errors ,
compute_plan_parameters ,
2020-10-14 12:17:03 +02:00
customer_has_credit_card_as_default_source ,
do_create_stripe_customer ,
2020-06-11 00:54:34 +02:00
get_discount_for_realm ,
get_latest_seat_count ,
2020-12-04 12:56:58 +01:00
get_price_per_license ,
2021-06-09 13:46:12 +02:00
get_realms_to_default_discount_dict ,
2020-06-11 00:54:34 +02:00
invoice_plan ,
invoice_plans_as_needed ,
2020-11-11 14:09:30 +01:00
is_realm_on_free_trial ,
2020-10-14 18:45:57 +02:00
is_sponsored_realm ,
2020-06-11 00:54:34 +02:00
make_end_of_cycle_updates_if_needed ,
next_month ,
process_initial_upgrade ,
sign_string ,
2020-10-14 12:17:03 +02:00
stripe_customer_has_credit_card_as_default_source ,
2020-06-11 00:54:34 +02:00
stripe_get_customer ,
unsign_string ,
2020-08-18 13:48:11 +02:00
update_billing_method_of_current_plan ,
2020-06-11 00:54:34 +02:00
update_license_ledger_for_automanaged_plan ,
2020-12-30 18:56:57 +01:00
update_license_ledger_for_manual_plan ,
2020-06-11 00:54:34 +02:00
update_license_ledger_if_needed ,
update_or_create_stripe_customer ,
2020-12-04 12:14:51 +01:00
update_sponsorship_status ,
2020-08-13 10:39:25 +02:00
void_all_open_invoices ,
2020-06-11 00:54:34 +02:00
)
from corporate . models import (
Customer ,
CustomerPlan ,
LicenseLedger ,
get_current_plan_by_customer ,
get_current_plan_by_realm ,
get_customer_by_realm ,
)
from zerver . lib . actions import (
do_activate_user ,
2021-03-08 13:22:43 +01:00
do_create_realm ,
2020-06-11 00:54:34 +02:00
do_create_user ,
do_deactivate_realm ,
do_deactivate_user ,
do_reactivate_realm ,
do_reactivate_user ,
)
2018-03-31 04:13:44 +02:00
from zerver . lib . test_classes import ZulipTestCase
2020-03-12 14:17:25 +01:00
from zerver . lib . test_helpers import reset_emails_in_zulip_realm
2020-06-11 00:54:34 +02:00
from zerver . lib . timestamp import datetime_to_timestamp , timestamp_to_datetime
2020-12-04 11:16:33 +01:00
from zerver . models import Message , Realm , RealmAuditLog , Recipient , UserProfile , get_realm
2018-03-31 04:13:44 +02:00
2021-02-12 08:20:45 +01:00
CallableT = TypeVar ( " CallableT " , bound = Callable [ . . . , Any ] )
2018-10-17 08:23:13 +02:00
2018-11-09 08:15:44 +01:00
STRIPE_FIXTURES_DIR = " corporate/tests/stripe_fixtures "
2018-07-26 16:10:07 +02:00
2018-10-17 08:23:13 +02:00
# TODO: check that this creates a token similar to what is created by our
# actual Stripe Checkout flows
2021-02-12 08:19:30 +01:00
def stripe_create_token ( card_number : str = " 4242424242424242 " ) - > stripe . Token :
2018-10-17 08:23:13 +02:00
return stripe . Token . create (
card = {
2018-10-18 20:04:45 +02:00
" number " : card_number ,
2018-10-17 08:23:13 +02:00
" exp_month " : 3 ,
" exp_year " : 2033 ,
" cvc " : " 333 " ,
" name " : " Ada Starr " ,
" address_line1 " : " Under the sea, " ,
" address_city " : " Pacific " ,
" address_zip " : " 33333 " ,
" address_country " : " United States " ,
2021-02-12 08:19:30 +01:00
}
)
2018-10-17 08:23:13 +02:00
2021-02-12 08:19:30 +01:00
def stripe_fixture_path (
decorated_function_name : str , mocked_function_name : str , call_count : int
) - > str :
2018-10-17 08:23:13 +02:00
# Make the eventual filename a bit shorter, and also we conventionally
# use test_* for the python test files
2021-02-12 08:20:45 +01:00
if decorated_function_name [ : 5 ] == " test_ " :
2018-10-17 08:23:13 +02:00
decorated_function_name = decorated_function_name [ 5 : ]
2020-06-10 06:40:53 +02:00
return f " { STRIPE_FIXTURES_DIR } / { decorated_function_name } -- { mocked_function_name [ 7 : ] } . { call_count } .json "
2018-11-09 08:15:44 +01:00
2021-02-12 08:19:30 +01:00
2018-11-09 08:15:44 +01:00
def fixture_files_for_function ( decorated_function : CallableT ) - > List [ str ] : # nocoverage
decorated_function_name = decorated_function . __name__
2021-02-12 08:20:45 +01:00
if decorated_function_name [ : 5 ] == " test_ " :
2018-11-09 08:15:44 +01:00
decorated_function_name = decorated_function_name [ 5 : ]
2021-02-12 08:19:30 +01:00
return sorted (
2021-02-12 08:20:45 +01:00
f " { STRIPE_FIXTURES_DIR } / { f } "
2021-02-12 08:19:30 +01:00
for f in os . listdir ( STRIPE_FIXTURES_DIR )
2021-02-12 08:20:45 +01:00
if f . startswith ( decorated_function_name + " -- " )
2021-02-12 08:19:30 +01:00
)
2018-10-17 08:23:13 +02:00
2021-02-12 08:19:30 +01:00
def generate_and_save_stripe_fixture (
decorated_function_name : str , mocked_function_name : str , mocked_function : CallableT
) - > Callable [ [ Any , Any ] , Any ] : # nocoverage
2018-10-17 08:23:13 +02:00
def _generate_and_save_stripe_fixture ( * args : Any , * * kwargs : Any ) - > Any :
# Note that mock is not the same as mocked_function, even though their
# definitions look the same
mock = operator . attrgetter ( mocked_function_name ) ( sys . modules [ __name__ ] )
2021-02-12 08:19:30 +01:00
fixture_path = stripe_fixture_path (
decorated_function_name , mocked_function_name , mock . call_count
)
2018-10-29 07:36:50 +01:00
try :
2020-03-13 15:45:47 +01:00
with responses . RequestsMock ( ) as request_mock :
request_mock . add_passthru ( " https://api.stripe.com " )
# Talk to Stripe
stripe_object = mocked_function ( * args , * * kwargs )
2018-10-29 07:36:50 +01:00
except stripe . error . StripeError as e :
2021-02-12 08:20:45 +01:00
with open ( fixture_path , " w " ) as f :
2018-10-29 07:36:50 +01:00
error_dict = e . __dict__
error_dict [ " headers " ] = dict ( error_dict [ " headers " ] )
2021-02-12 08:19:30 +01:00
f . write (
2021-02-12 08:20:45 +01:00
json . dumps ( error_dict , indent = 2 , separators = ( " , " , " : " ) , sort_keys = True ) + " \n "
2021-02-12 08:19:30 +01:00
)
2018-10-29 07:36:50 +01:00
raise e
2021-02-12 08:20:45 +01:00
with open ( fixture_path , " w " ) as f :
2018-11-16 16:49:40 +01:00
if stripe_object is not None :
f . write ( str ( stripe_object ) + " \n " )
else :
f . write ( " {} \n " )
2018-10-17 08:23:13 +02:00
return stripe_object
2021-02-12 08:19:30 +01:00
2018-10-17 08:23:13 +02:00
return _generate_and_save_stripe_fixture
2021-02-12 08:19:30 +01:00
def read_stripe_fixture (
decorated_function_name : str , mocked_function_name : str
) - > Callable [ [ Any , Any ] , Any ] :
2018-10-17 08:23:13 +02:00
def _read_stripe_fixture ( * args : Any , * * kwargs : Any ) - > Any :
mock = operator . attrgetter ( mocked_function_name ) ( sys . modules [ __name__ ] )
2021-02-12 08:19:30 +01:00
fixture_path = stripe_fixture_path (
decorated_function_name , mocked_function_name , mock . call_count
)
2020-08-07 01:09:47 +02:00
with open ( fixture_path , " rb " ) as f :
fixture = orjson . loads ( f . read ( ) )
2018-10-29 07:36:50 +01:00
# Check for StripeError fixtures
if " json_body " in fixture :
requestor = stripe . api_requestor . APIRequestor ( )
# This function will raise the relevant StripeError according to the fixture
2021-02-12 08:19:30 +01:00
requestor . interpret_response (
fixture [ " http_body " ] , fixture [ " http_status " ] , fixture [ " headers " ]
)
2018-10-29 07:36:50 +01:00
return stripe . util . convert_to_stripe_object ( fixture )
2021-02-12 08:19:30 +01:00
2018-10-17 08:23:13 +02:00
return _read_stripe_fixture
2021-02-12 08:19:30 +01:00
2018-11-26 23:07:36 +01:00
def delete_fixture_data ( decorated_function : CallableT ) - > None : # nocoverage
for fixture_file in fixture_files_for_function ( decorated_function ) :
os . remove ( fixture_file )
2021-02-12 08:19:30 +01:00
def normalize_fixture_data (
decorated_function : CallableT , tested_timestamp_fields : Sequence [ str ] = [ ]
) - > None : # nocoverage
2018-11-09 08:15:44 +01:00
# stripe ids are all of the form cus_D7OT2jf5YAtZQ2
id_lengths = [
2021-02-12 08:20:45 +01:00
( " cus " , 14 ) ,
( " sub " , 14 ) ,
( " si " , 14 ) ,
( " sli " , 14 ) ,
( " req " , 14 ) ,
( " tok " , 24 ) ,
( " card " , 24 ) ,
( " txn " , 24 ) ,
( " ch " , 24 ) ,
( " in " , 24 ) ,
( " ii " , 24 ) ,
( " test " , 12 ) ,
( " src_client_secret " , 24 ) ,
( " src " , 24 ) ,
( " invst " , 26 ) ,
( " acct " , 16 ) ,
( " rcpt " , 31 ) ,
2021-02-12 08:19:30 +01:00
]
2018-11-09 08:15:44 +01:00
# We'll replace cus_D7OT2jf5YAtZQ2 with something like cus_NORMALIZED0001
pattern_translations = {
2020-06-13 08:59:37 +02:00
f " { prefix } _[A-Za-z0-9] {{ { length } }} " : f " { prefix } _NORMALIZED%0 { length - 10 } d "
2018-11-09 08:15:44 +01:00
for prefix , length in id_lengths
}
# We'll replace "invoice_prefix": "A35BC4Q" with something like "invoice_prefix": "NORMA01"
2021-02-12 08:19:30 +01:00
pattern_translations . update (
{
2021-02-12 08:20:45 +01:00
' " invoice_prefix " : " ([A-Za-z0-9] { 7,8}) " ' : " NORMA %02d " ,
' " fingerprint " : " ([A-Za-z0-9] {16} ) " ' : " NORMALIZED %06d " ,
' " number " : " ([A-Za-z0-9] { 7,8}-[A-Za-z0-9] {4} ) " ' : " NORMALI- %04d " ,
' " address " : " ([A-Za-z0-9] {9} -test_[A-Za-z0-9] {12} ) " ' : " 000000000-test_NORMALIZED %02d " ,
2021-02-12 08:19:30 +01:00
# Don't use (..) notation, since the matched strings may be small integers that will also match
# elsewhere in the file
' " realm_id " : " [0-9]+ " ' : ' " realm_id " : " %d " ' ,
2021-06-18 16:34:20 +02:00
r ' " account_name " : " [ \ w \ s]+ " ' : ' " account_name " : " NORMALIZED- %d " ' ,
2021-02-12 08:19:30 +01:00
}
)
2018-12-04 00:29:23 +01:00
# Normalizing across all timestamps still causes a lot of variance run to run, which is
# why we're doing something a bit more complicated
for i , timestamp_field in enumerate ( tested_timestamp_fields ) :
# Don't use (..) notation, since the matched timestamp can easily appear in other fields
pattern_translations [
2020-06-10 06:41:04 +02:00
f ' " { timestamp_field } " : 1[5-9][0-9] {{ 8 }} (?![0-9-]) '
2020-06-13 08:59:37 +02:00
] = f ' " { timestamp_field } " : 1 { i + 1 : 02 } %07d '
2018-11-09 08:15:44 +01:00
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
normalized_values : Dict [ str , Dict [ str , str ] ] = {
pattern : { } for pattern in pattern_translations . keys ( )
}
2018-11-09 08:15:44 +01:00
for fixture_file in fixture_files_for_function ( decorated_function ) :
2020-04-09 21:51:58 +02:00
with open ( fixture_file ) as f :
2018-11-09 08:15:44 +01:00
file_content = f . read ( )
for pattern , translation in pattern_translations . items ( ) :
for match in re . findall ( pattern , file_content ) :
if match not in normalized_values [ pattern ] :
2021-02-12 08:19:30 +01:00
normalized_values [ pattern ] [ match ] = translation % (
len ( normalized_values [ pattern ] ) + 1 ,
)
2018-11-09 08:15:44 +01:00
file_content = file_content . replace ( match , normalized_values [ pattern ] [ match ] )
2021-02-12 08:20:45 +01:00
file_content = re . sub ( r ' (?<= " risk_score " : )( \ d+) ' , " 0 " , file_content )
file_content = re . sub ( r ' (?<= " times_redeemed " : )( \ d+) ' , " 0 " , file_content )
2021-02-12 08:19:30 +01:00
file_content = re . sub (
r ' (?<= " idempotency-key " : ) " ([0-9a-f] {8} -[0-9a-f] {4} -[0-9a-f-]*) " ' ,
' " 00000000-0000-0000-0000-000000000000 " ' ,
file_content ,
)
2018-12-01 16:57:52 +01:00
# Dates
2018-12-03 22:16:09 +01:00
file_content = re . sub ( r ' (?<= " Date " : ) " (.* GMT) " ' , ' " NORMALIZED DATETIME " ' , file_content )
2021-02-12 08:20:45 +01:00
file_content = re . sub ( r " [0-3] \ d [A-Z][a-z] {2} 20[1-2] \ d " , " NORMALIZED DATE " , file_content )
2018-12-01 16:57:52 +01:00
# IP addresses
2018-11-09 08:15:44 +01:00
file_content = re . sub ( r ' " \ d { 1,3} \ . \ d { 1,3} \ . \ d { 1,3} \ . \ d { 1,3} " ' , ' " 0.0.0.0 " ' , file_content )
2018-12-04 00:29:23 +01:00
# All timestamps not in tested_timestamp_fields
2021-02-12 08:20:45 +01:00
file_content = re . sub ( r " : (1[5-9][0-9] {8} )(?![0-9-]) " , " : 1000000000 " , file_content )
2018-12-01 16:57:52 +01:00
2018-11-09 08:15:44 +01:00
with open ( fixture_file , " w " ) as f :
f . write ( file_content )
2021-02-12 08:19:30 +01:00
MOCKED_STRIPE_FUNCTION_NAMES = [
f " stripe. { name } "
for name in [
" Charge.create " ,
" Charge.list " ,
" Coupon.create " ,
" Customer.create " ,
" Customer.retrieve " ,
" Customer.save " ,
" Invoice.create " ,
" Invoice.finalize_invoice " ,
" Invoice.list " ,
" Invoice.pay " ,
" Invoice.upcoming " ,
" Invoice.void_invoice " ,
" InvoiceItem.create " ,
" InvoiceItem.list " ,
" Plan.create " ,
" Product.create " ,
" Subscription.create " ,
" Subscription.delete " ,
" Subscription.retrieve " ,
" Subscription.save " ,
" Token.create " ,
]
]
def mock_stripe (
tested_timestamp_fields : Sequence [ str ] = [ ] , generate : Optional [ bool ] = None
) - > Callable [ [ CallableT ] , CallableT ] :
2018-11-12 17:18:36 +01:00
def _mock_stripe ( decorated_function : CallableT ) - > CallableT :
2018-11-07 11:28:36 +01:00
generate_fixture = generate
2018-10-30 11:33:24 +01:00
if generate_fixture is None :
2020-03-13 15:45:47 +01:00
generate_fixture = settings . GENERATE_STRIPE_FIXTURES
2018-12-03 19:23:13 +01:00
for mocked_function_name in MOCKED_STRIPE_FUNCTION_NAMES :
2018-11-07 11:28:36 +01:00
mocked_function = operator . attrgetter ( mocked_function_name ) ( sys . modules [ __name__ ] )
if generate_fixture :
side_effect = generate_and_save_stripe_fixture (
2021-02-12 08:19:30 +01:00
decorated_function . __name__ , mocked_function_name , mocked_function
) # nocoverage
2018-11-07 11:28:36 +01:00
else :
side_effect = read_stripe_fixture ( decorated_function . __name__ , mocked_function_name )
2021-02-12 08:19:30 +01:00
decorated_function = cast (
CallableT , patch ( mocked_function_name , side_effect = side_effect ) ( decorated_function )
)
2018-10-17 08:23:13 +02:00
@wraps ( decorated_function )
2020-06-24 02:10:50 +02:00
def wrapped ( * args : object , * * kwargs : object ) - > object :
2018-11-09 08:15:44 +01:00
if generate_fixture : # nocoverage
2018-11-26 23:07:36 +01:00
delete_fixture_data ( decorated_function )
val = decorated_function ( * args , * * kwargs )
2018-12-04 00:29:23 +01:00
normalize_fixture_data ( decorated_function , tested_timestamp_fields )
2018-11-26 23:07:36 +01:00
return val
else :
return decorated_function ( * args , * * kwargs )
2021-02-12 08:19:30 +01:00
2018-11-12 17:18:36 +01:00
return cast ( CallableT , wrapped )
2021-02-12 08:19:30 +01:00
2018-10-17 08:23:13 +02:00
return _mock_stripe
2021-02-12 08:19:30 +01:00
2018-08-09 21:38:22 +02:00
# A Kandra is a fictional character that can become anything. Used as a
# wildcard when testing for equality.
2020-04-09 21:51:58 +02:00
class Kandra : # nocoverage: TODO
2018-08-09 21:38:22 +02:00
def __eq__ ( self , other : Any ) - > bool :
return True
2021-02-12 08:19:30 +01:00
2019-01-27 21:16:02 +01:00
class StripeTestCase ( ZulipTestCase ) :
2018-12-02 19:05:34 +01:00
def setUp ( self , * mocks : Mock ) - > None :
2019-10-19 20:47:00 +02:00
super ( ) . setUp ( )
2020-03-12 14:17:25 +01:00
reset_emails_in_zulip_realm ( )
2021-02-12 08:20:45 +01:00
realm = get_realm ( " zulip " )
2020-05-18 21:00:25 +02:00
# Explicitly limit our active users to 6 regular users,
# to make seat_count less prone to changes in our test data.
# We also keep a guest user and a bot to make the data
# slightly realistic.
active_emails = [
2021-02-12 08:20:45 +01:00
self . example_email ( " AARON " ) ,
self . example_email ( " cordelia " ) ,
self . example_email ( " hamlet " ) ,
self . example_email ( " iago " ) ,
self . example_email ( " othello " ) ,
self . example_email ( " desdemona " ) ,
self . example_email ( " polonius " ) , # guest
self . example_email ( " default_bot " ) , # bot
2020-05-18 21:00:25 +02:00
]
2020-05-19 01:59:15 +02:00
# Deactivate all users in our realm that aren't in our whitelist.
2021-02-14 00:03:40 +01:00
for user_profile in UserProfile . objects . filter ( realm_id = realm . id ) . exclude (
email__in = active_emails
) :
do_deactivate_user ( user_profile , acting_user = None )
2020-05-18 21:00:25 +02:00
# sanity check our 8 expected users are active
self . assertEqual (
UserProfile . objects . filter ( realm = realm , is_active = True ) . count ( ) ,
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
8 ,
2020-05-18 21:00:25 +02:00
)
2020-05-19 01:59:15 +02:00
# Make sure we have active users outside our realm (to make
# sure relevant queries restrict on realm).
self . assertEqual (
UserProfile . objects . exclude ( realm = realm ) . filter ( is_active = True ) . count ( ) ,
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
10 ,
2020-05-19 01:59:15 +02:00
)
2020-05-18 21:00:25 +02:00
# Our seat count excludes our guest user and bot, and
# we want this to be predictable for certain tests with
# arithmetic calculations.
2019-10-07 19:21:29 +02:00
self . assertEqual ( get_latest_seat_count ( realm ) , 6 )
2019-01-30 18:58:53 +01:00
self . seat_count = 6
2018-11-29 03:24:19 +01:00
self . signed_seat_count , self . salt = sign_string ( str ( self . seat_count ) )
2018-12-15 09:33:25 +01:00
# Choosing dates with corresponding timestamps below 1500000000 so that they are
# not caught by our timestamp normalization regex in normalize_fixture_data
2020-06-05 06:55:20 +02:00
self . now = datetime ( 2012 , 1 , 2 , 3 , 4 , 5 , tzinfo = timezone . utc )
self . next_month = datetime ( 2012 , 2 , 2 , 3 , 4 , 5 , tzinfo = timezone . utc )
self . next_year = datetime ( 2013 , 1 , 2 , 3 , 4 , 5 , tzinfo = timezone . utc )
2018-03-31 04:13:44 +02:00
2018-08-08 16:35:33 +02:00
def get_signed_seat_count_from_response ( self , response : HttpResponse ) - > Optional [ str ] :
2021-02-12 08:19:30 +01:00
match = re . search (
2021-02-12 08:20:45 +01:00
r " name= \" signed_seat_count \" value= \" (.+) \" " , response . content . decode ( " utf-8 " )
2021-02-12 08:19:30 +01:00
)
2018-08-08 16:35:33 +02:00
return match . group ( 1 ) if match else None
def get_salt_from_response ( self , response : HttpResponse ) - > Optional [ str ] :
2021-02-12 08:20:45 +01:00
match = re . search ( r " name= \" salt \" value= \" ( \ w+) \" " , response . content . decode ( " utf-8 " ) )
2018-08-08 16:35:33 +02:00
return match . group ( 1 ) if match else None
2021-02-12 08:19:30 +01:00
def upgrade (
self ,
invoice : bool = False ,
talk_to_stripe : bool = True ,
realm : Optional [ Realm ] = None ,
del_args : Sequence [ str ] = [ ] ,
* * kwargs : Any ,
) - > HttpResponse :
2018-11-29 03:15:27 +01:00
host_args = { }
2018-12-15 09:33:25 +01:00
if realm is not None : # nocoverage: TODO
2021-02-12 08:20:45 +01:00
host_args [ " HTTP_HOST " ] = realm . host
2020-08-07 04:45:55 +02:00
response = self . client_get ( " /upgrade/ " , { } , * * host_args )
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
params : Dict [ str , Any ] = {
2021-02-12 08:20:45 +01:00
" schedule " : " annual " ,
" signed_seat_count " : self . get_signed_seat_count_from_response ( response ) ,
" salt " : self . get_salt_from_response ( response ) ,
2021-02-12 08:19:30 +01:00
}
2018-11-29 03:15:27 +01:00
if invoice : # send_invoice
2020-09-03 05:32:15 +02:00
params . update (
2021-02-12 08:20:45 +01:00
billing_modality = " send_invoice " ,
2020-12-23 17:08:27 +01:00
licenses = kwargs . get ( " licenses " , 123 ) ,
2020-09-03 05:32:15 +02:00
)
2018-11-29 03:15:27 +01:00
else : # charge_automatically
stripe_token = None
if not talk_to_stripe :
2021-02-12 08:20:45 +01:00
stripe_token = " token "
stripe_token = kwargs . get ( " stripe_token " , stripe_token )
2018-11-29 03:15:27 +01:00
if stripe_token is None :
stripe_token = stripe_create_token ( ) . id
2020-09-03 05:32:15 +02:00
params . update (
2021-02-12 08:20:45 +01:00
billing_modality = " charge_automatically " ,
license_management = " automatic " ,
2020-09-03 05:32:15 +02:00
stripe_token = stripe_token ,
)
2018-12-07 18:43:22 +01:00
2018-11-29 03:15:27 +01:00
params . update ( kwargs )
2018-12-22 05:29:25 +01:00
for key in del_args :
if key in params :
del params [ key ]
2018-12-07 18:43:22 +01:00
return self . client_post ( " /json/billing/upgrade " , params , * * host_args )
2018-11-29 03:15:27 +01:00
2019-01-27 21:16:02 +01:00
# Upgrade without talking to Stripe
def local_upgrade ( self , * args : Any ) - > None :
2020-06-23 00:31:30 +02:00
class StripeMock ( Mock ) :
2021-02-12 08:19:30 +01:00
def __init__ ( self , depth : int = 1 ) :
2020-06-23 00:31:30 +02:00
super ( ) . __init__ ( spec = stripe . Card )
2021-02-12 08:20:45 +01:00
self . id = " id "
self . created = " 1000 "
self . last4 = " 4242 "
2019-01-27 21:16:02 +01:00
if depth == 1 :
self . source = StripeMock ( depth = 2 )
def upgrade_func ( * args : Any ) - > Any :
2021-02-12 08:20:45 +01:00
return process_initial_upgrade ( self . example_user ( " hamlet " ) , * args [ : 4 ] )
2019-01-27 21:16:02 +01:00
for mocked_function_name in MOCKED_STRIPE_FUNCTION_NAMES :
upgrade_func = patch ( mocked_function_name , return_value = StripeMock ( ) ) ( upgrade_func )
upgrade_func ( * args )
2021-02-12 08:19:30 +01:00
2019-01-27 21:16:02 +01:00
class StripeTest ( StripeTestCase ) :
2020-12-23 21:45:16 +01:00
def test_catch_stripe_errors ( self ) - > None :
2018-03-31 04:13:44 +02:00
@catch_stripe_errors
def raise_invalid_request_error ( ) - > None :
2021-02-12 08:19:30 +01:00
raise stripe . error . InvalidRequestError ( " message " , " param " , " code " , json_body = { } )
2021-02-12 08:20:45 +01:00
with self . assertLogs ( " corporate.stripe " , " ERROR " ) as error_log :
2020-12-23 21:45:16 +01:00
with self . assertRaises ( BillingError ) as context :
raise_invalid_request_error ( )
2021-02-12 08:20:45 +01:00
self . assertEqual ( " other stripe error " , context . exception . description )
2021-02-12 08:19:30 +01:00
self . assertEqual (
2021-02-12 08:20:45 +01:00
error_log . output , [ " ERROR:corporate.stripe:Stripe error: None None None None " ]
2021-02-12 08:19:30 +01:00
)
2018-03-31 04:13:44 +02:00
@catch_stripe_errors
def raise_card_error ( ) - > None :
error_message = " The card number is not a valid credit card number. "
json_body = { " error " : { " message " : error_message } }
2021-02-12 08:19:30 +01:00
raise stripe . error . CardError (
error_message , " number " , " invalid_number " , json_body = json_body
)
2021-02-12 08:20:45 +01:00
with self . assertLogs ( " corporate.stripe " , " INFO " ) as info_log :
2020-12-23 21:45:16 +01:00
with self . assertRaises ( StripeCardError ) as context :
raise_card_error ( )
2021-02-12 08:20:45 +01:00
self . assertIn ( " not a valid credit card " , context . exception . message )
self . assertEqual ( " card error " , context . exception . description )
2021-02-12 08:19:30 +01:00
self . assertEqual (
2021-02-12 08:20:45 +01:00
info_log . output , [ " INFO:corporate.stripe:Stripe card error: None None None None " ]
2021-02-12 08:19:30 +01:00
)
2018-03-31 04:13:44 +02:00
2018-10-18 20:31:01 +02:00
def test_billing_not_enabled ( self ) - > None :
2021-02-12 08:20:45 +01:00
iago = self . example_user ( " iago " )
2018-10-18 20:31:01 +02:00
with self . settings ( BILLING_ENABLED = False ) :
2020-03-06 18:40:46 +01:00
self . login_user ( iago )
2020-07-15 22:18:32 +02:00
response = self . client_get ( " /upgrade/ " , follow = True )
self . assertEqual ( response . status_code , 404 )
2018-10-18 20:31:01 +02:00
2018-12-04 00:29:23 +01:00
@mock_stripe ( tested_timestamp_fields = [ " created " ] )
2018-12-15 09:33:25 +01:00
def test_upgrade_by_card ( self , * mocks : Mock ) - > None :
2018-07-25 16:37:07 +02:00
user = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( user )
2018-03-31 04:13:44 +02:00
response = self . client_get ( " /upgrade/ " )
2021-02-12 08:20:45 +01:00
self . assert_in_success_response ( [ " Pay annually " ] , response )
2018-10-24 06:09:01 +02:00
self . assertNotEqual ( user . realm . plan_type , Realm . STANDARD )
2018-10-17 08:23:13 +02:00
self . assertFalse ( Customer . objects . filter ( realm = user . realm ) . exists ( ) )
2018-08-08 16:35:33 +02:00
2018-03-31 04:13:44 +02:00
# Click "Make payment" in Stripe Checkout
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2018-12-15 09:33:25 +01:00
self . upgrade ( )
2018-10-17 08:23:13 +02:00
2018-12-15 09:33:25 +01:00
# Check that we correctly created a Customer object in Stripe
2021-02-12 08:19:30 +01:00
stripe_customer = stripe_get_customer (
Customer . objects . get ( realm = user . realm ) . stripe_customer_id
)
2021-02-12 08:20:45 +01:00
self . assertEqual ( stripe_customer . default_source . id [ : 5 ] , " card_ " )
2020-10-14 12:17:03 +02:00
self . assertTrue ( stripe_customer_has_credit_card_as_default_source ( stripe_customer ) )
2018-10-17 08:23:13 +02:00
self . assertEqual ( stripe_customer . description , " zulip (Zulip Dev) " )
self . assertEqual ( stripe_customer . discount , None )
self . assertEqual ( stripe_customer . email , user . email )
2019-08-19 10:41:19 +02:00
metadata_dict = dict ( stripe_customer . metadata )
2021-02-12 08:20:45 +01:00
self . assertEqual ( metadata_dict [ " realm_str " ] , " zulip " )
2019-08-19 10:41:19 +02:00
try :
2021-02-12 08:20:45 +01:00
int ( metadata_dict [ " realm_id " ] )
2019-08-19 10:41:19 +02:00
except ValueError : # nocoverage
raise AssertionError ( " realm_id is not a number " )
2018-12-15 09:33:25 +01:00
# Check Charges in Stripe
2020-09-02 07:55:39 +02:00
[ charge ] = stripe . Charge . list ( customer = stripe_customer . id )
self . assertEqual ( charge . amount , 8000 * self . seat_count )
2018-12-15 09:33:25 +01:00
# TODO: fix Decimal
2021-02-12 08:19:30 +01:00
self . assertEqual (
charge . description , f " Upgrade to Zulip Standard, $80.0 x { self . seat_count } "
)
2020-09-02 07:55:39 +02:00
self . assertEqual ( charge . receipt_email , user . email )
self . assertEqual ( charge . statement_descriptor , " Zulip Standard " )
2018-12-15 09:33:25 +01:00
# Check Invoices in Stripe
2020-09-02 07:55:39 +02:00
[ invoice ] = stripe . Invoice . list ( customer = stripe_customer . id )
self . assertIsNotNone ( invoice . status_transitions . finalized_at )
2018-12-15 09:33:25 +01:00
invoice_params = {
# auto_advance is False because the invoice has been paid
2021-02-12 08:20:45 +01:00
" amount_due " : 0 ,
" amount_paid " : 0 ,
" auto_advance " : False ,
" billing " : " charge_automatically " ,
" charge " : None ,
" status " : " paid " ,
" total " : 0 ,
2021-02-12 08:19:30 +01:00
}
2018-12-15 09:33:25 +01:00
for key , value in invoice_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( invoice . get ( key ) , value )
2018-12-15 09:33:25 +01:00
# Check Line Items on Stripe Invoice
2020-09-02 07:55:39 +02:00
[ item0 , item1 ] = invoice . lines
2018-12-15 09:33:25 +01:00
line_item_params = {
2021-02-12 08:20:45 +01:00
" amount " : 8000 * self . seat_count ,
" description " : " Zulip Standard " ,
" discountable " : False ,
" period " : {
" end " : datetime_to_timestamp ( self . next_year ) ,
" start " : datetime_to_timestamp ( self . now ) ,
2021-02-12 08:19:30 +01:00
} ,
2018-12-15 09:33:25 +01:00
# There's no unit_amount on Line Items, probably because it doesn't show up on the
# user-facing invoice. We could pull the Invoice Item instead and test unit_amount there,
# but testing the amount and quantity seems sufficient.
2021-02-12 08:20:45 +01:00
" plan " : None ,
" proration " : False ,
" quantity " : self . seat_count ,
2021-02-12 08:19:30 +01:00
}
2018-12-15 09:33:25 +01:00
for key , value in line_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( item0 . get ( key ) , value )
2018-12-15 09:33:25 +01:00
line_item_params = {
2021-02-12 08:20:45 +01:00
" amount " : - 8000 * self . seat_count ,
" description " : " Payment (Card ending in 4242) " ,
" discountable " : False ,
" plan " : None ,
" proration " : False ,
" quantity " : 1 ,
2021-02-12 08:19:30 +01:00
}
2018-12-15 09:33:25 +01:00
for key , value in line_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( item1 . get ( key ) , value )
2018-12-15 09:33:25 +01:00
2018-12-28 07:20:30 +01:00
# Check that we correctly populated Customer, CustomerPlan, and LicenseLedger in Zulip
customer = Customer . objects . get ( stripe_customer_id = stripe_customer . id , realm = user . realm )
plan = CustomerPlan . objects . get (
2021-02-12 08:19:30 +01:00
customer = customer ,
automanage_licenses = True ,
price_per_license = 8000 ,
fixed_price = None ,
discount = None ,
billing_cycle_anchor = self . now ,
billing_schedule = CustomerPlan . ANNUAL ,
invoiced_through = LicenseLedger . objects . first ( ) ,
next_invoice_date = self . next_month ,
tier = CustomerPlan . STANDARD ,
status = CustomerPlan . ACTIVE ,
)
2018-12-28 07:20:30 +01:00
LicenseLedger . objects . get (
2021-02-12 08:19:30 +01:00
plan = plan ,
is_renewal = True ,
event_time = self . now ,
licenses = self . seat_count ,
licenses_at_next_renewal = self . seat_count ,
)
2018-12-15 09:33:25 +01:00
# Check RealmAuditLog
2021-02-12 08:19:30 +01:00
audit_log_entries = list (
RealmAuditLog . objects . filter ( acting_user = user )
2021-02-12 08:20:45 +01:00
. values_list ( " event_type " , " event_time " )
. order_by ( " id " )
2021-02-12 08:19:30 +01:00
)
self . assertEqual (
2020-12-04 10:54:15 +01:00
audit_log_entries [ : 3 ] ,
2021-02-12 08:19:30 +01:00
[
(
RealmAuditLog . STRIPE_CUSTOMER_CREATED ,
timestamp_to_datetime ( stripe_customer . created ) ,
) ,
( RealmAuditLog . STRIPE_CARD_CHANGED , timestamp_to_datetime ( stripe_customer . created ) ) ,
( RealmAuditLog . CUSTOMER_PLAN_CREATED , self . now ) ,
] ,
)
2020-12-04 10:54:15 +01:00
self . assertEqual ( audit_log_entries [ 3 ] [ 0 ] , RealmAuditLog . REALM_PLAN_TYPE_CHANGED )
2021-02-12 08:19:30 +01:00
self . assertEqual (
orjson . loads (
RealmAuditLog . objects . filter ( event_type = RealmAuditLog . CUSTOMER_PLAN_CREATED )
2021-02-12 08:20:45 +01:00
. values_list ( " extra_data " , flat = True )
2021-02-12 08:19:30 +01:00
. first ( )
2021-02-12 08:20:45 +01:00
) [ " automanage_licenses " ] ,
2021-02-12 08:19:30 +01:00
True ,
)
2018-06-28 00:48:51 +02:00
# Check that we correctly updated Realm
realm = get_realm ( " zulip " )
2018-10-24 06:09:01 +02:00
self . assertEqual ( realm . plan_type , Realm . STANDARD )
self . assertEqual ( realm . max_invites , Realm . INVITES_STANDARD_REALM_DAILY_MAX )
2018-03-31 04:13:44 +02:00
# Check that we can no longer access /upgrade
response = self . client_get ( " /upgrade/ " )
self . assertEqual ( response . status_code , 302 )
2021-02-12 08:20:45 +01:00
self . assertEqual ( " /billing/ " , response . url )
2018-03-31 04:13:44 +02:00
2018-12-23 09:10:57 +01:00
# Check /billing has the correct information
2021-02-12 08:20:45 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
2019-01-26 20:45:26 +01:00
response = self . client_get ( " /billing/ " )
2021-02-12 08:20:45 +01:00
self . assert_not_in_success_response ( [ " Pay annually " ] , response )
2018-12-23 09:10:57 +01:00
for substring in [
2021-02-12 08:20:45 +01:00
" Zulip Standard " ,
2021-02-12 08:19:30 +01:00
str ( self . seat_count ) ,
2021-02-12 08:20:45 +01:00
" You are using " ,
f " { self . seat_count } of { self . seat_count } licenses " ,
" Licenses are automatically managed by Zulip; when you add " ,
" Your plan will renew on " ,
" January 2, 2013 " ,
f " $ { 80 * self . seat_count } .00 " ,
2021-05-14 16:48:00 +02:00
f " Billing email: <strong> { user . email } </strong> " ,
2021-02-12 08:20:45 +01:00
" Visa ending in 4242 " ,
" Update card " ,
2021-02-12 08:19:30 +01:00
] :
2018-12-23 09:10:57 +01:00
self . assert_in_response ( substring , response )
2018-12-15 09:33:25 +01:00
2020-12-23 17:08:27 +01:00
self . assert_not_in_success_response (
[
" You can only increase the number of licenses. " ,
" Number of licenses " ,
" Licenses in next renewal " ,
] ,
response ,
)
2018-12-15 09:33:25 +01:00
@mock_stripe ( tested_timestamp_fields = [ " created " ] )
def test_upgrade_by_invoice ( self , * mocks : Mock ) - > None :
user = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( user )
2018-12-15 09:33:25 +01:00
# Click "Make payment" in Stripe Checkout
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2018-12-15 09:33:25 +01:00
self . upgrade ( invoice = True )
# Check that we correctly created a Customer in Stripe
2021-02-12 08:19:30 +01:00
stripe_customer = stripe_get_customer (
Customer . objects . get ( realm = user . realm ) . stripe_customer_id
)
2020-10-14 12:17:03 +02:00
self . assertFalse ( stripe_customer_has_credit_card_as_default_source ( stripe_customer ) )
2018-12-15 09:33:25 +01:00
# It can take a second for Stripe to attach the source to the customer, and in
# particular it may not be attached at the time stripe_get_customer is called above,
# causing test flakes.
# So commenting the next line out, but leaving it here so future readers know what
# is supposed to happen here
# self.assertEqual(stripe_customer.default_source.type, 'ach_credit_transfer')
# Check Charges in Stripe
self . assertFalse ( stripe . Charge . list ( customer = stripe_customer . id ) )
# Check Invoices in Stripe
2020-09-02 07:55:39 +02:00
[ invoice ] = stripe . Invoice . list ( customer = stripe_customer . id )
self . assertIsNotNone ( invoice . due_date )
self . assertIsNotNone ( invoice . status_transitions . finalized_at )
2018-12-15 09:33:25 +01:00
invoice_params = {
2021-02-12 08:20:45 +01:00
" amount_due " : 8000 * 123 ,
" amount_paid " : 0 ,
" attempt_count " : 0 ,
" auto_advance " : True ,
" billing " : " send_invoice " ,
" statement_descriptor " : " Zulip Standard " ,
" status " : " open " ,
" total " : 8000 * 123 ,
2021-02-12 08:19:30 +01:00
}
2018-12-15 09:33:25 +01:00
for key , value in invoice_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( invoice . get ( key ) , value )
2018-12-15 09:33:25 +01:00
# Check Line Items on Stripe Invoice
2020-09-02 07:55:39 +02:00
[ item ] = invoice . lines
2018-12-15 09:33:25 +01:00
line_item_params = {
2021-02-12 08:20:45 +01:00
" amount " : 8000 * 123 ,
" description " : " Zulip Standard " ,
" discountable " : False ,
" period " : {
" end " : datetime_to_timestamp ( self . next_year ) ,
" start " : datetime_to_timestamp ( self . now ) ,
2021-02-12 08:19:30 +01:00
} ,
2021-02-12 08:20:45 +01:00
" plan " : None ,
" proration " : False ,
" quantity " : 123 ,
2021-02-12 08:19:30 +01:00
}
2018-12-15 09:33:25 +01:00
for key , value in line_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( item . get ( key ) , value )
2018-12-15 09:33:25 +01:00
2018-12-28 07:20:30 +01:00
# Check that we correctly populated Customer, CustomerPlan and LicenseLedger in Zulip
customer = Customer . objects . get ( stripe_customer_id = stripe_customer . id , realm = user . realm )
plan = CustomerPlan . objects . get (
2021-02-12 08:19:30 +01:00
customer = customer ,
automanage_licenses = False ,
charge_automatically = False ,
price_per_license = 8000 ,
fixed_price = None ,
discount = None ,
billing_cycle_anchor = self . now ,
billing_schedule = CustomerPlan . ANNUAL ,
invoiced_through = LicenseLedger . objects . first ( ) ,
next_invoice_date = self . next_year ,
tier = CustomerPlan . STANDARD ,
status = CustomerPlan . ACTIVE ,
)
2018-12-28 07:20:30 +01:00
LicenseLedger . objects . get (
2021-02-12 08:19:30 +01:00
plan = plan ,
is_renewal = True ,
event_time = self . now ,
licenses = 123 ,
licenses_at_next_renewal = 123 ,
)
2018-12-15 09:33:25 +01:00
# Check RealmAuditLog
2021-02-12 08:19:30 +01:00
audit_log_entries = list (
RealmAuditLog . objects . filter ( acting_user = user )
2021-02-12 08:20:45 +01:00
. values_list ( " event_type " , " event_time " )
. order_by ( " id " )
2021-02-12 08:19:30 +01:00
)
self . assertEqual (
2020-12-04 10:54:15 +01:00
audit_log_entries [ : 2 ] ,
2021-02-12 08:19:30 +01:00
[
(
RealmAuditLog . STRIPE_CUSTOMER_CREATED ,
timestamp_to_datetime ( stripe_customer . created ) ,
) ,
( RealmAuditLog . CUSTOMER_PLAN_CREATED , self . now ) ,
] ,
)
2020-12-04 10:54:15 +01:00
self . assertEqual ( audit_log_entries [ 2 ] [ 0 ] , RealmAuditLog . REALM_PLAN_TYPE_CHANGED )
2021-02-12 08:19:30 +01:00
self . assertEqual (
orjson . loads (
RealmAuditLog . objects . filter ( event_type = RealmAuditLog . CUSTOMER_PLAN_CREATED )
2021-02-12 08:20:45 +01:00
. values_list ( " extra_data " , flat = True )
2021-02-12 08:19:30 +01:00
. first ( )
2021-02-12 08:20:45 +01:00
) [ " automanage_licenses " ] ,
2021-02-12 08:19:30 +01:00
False ,
)
2018-12-15 09:33:25 +01:00
# Check that we correctly updated Realm
realm = get_realm ( " zulip " )
self . assertEqual ( realm . plan_type , Realm . STANDARD )
self . assertEqual ( realm . max_invites , Realm . INVITES_STANDARD_REALM_DAILY_MAX )
# Check that we can no longer access /upgrade
response = self . client_get ( " /upgrade/ " )
self . assertEqual ( response . status_code , 302 )
2021-02-12 08:20:45 +01:00
self . assertEqual ( " /billing/ " , response . url )
2018-12-15 09:33:25 +01:00
2018-12-23 09:10:57 +01:00
# Check /billing has the correct information
2021-02-12 08:20:45 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
2019-01-26 20:45:26 +01:00
response = self . client_get ( " /billing/ " )
2021-02-12 08:20:45 +01:00
self . assert_not_in_success_response ( [ " Pay annually " , " Update card " ] , response )
2018-12-23 09:10:57 +01:00
for substring in [
2021-02-12 08:20:45 +01:00
" Zulip Standard " ,
2021-02-12 08:19:30 +01:00
str ( 123 ) ,
2021-02-12 08:20:45 +01:00
" You are using " ,
f " { self . seat_count } of { 123 } licenses " ,
" Licenses are manually managed. You will not be able to add " ,
" Your plan will renew on " ,
" January 2, 2013 " ,
" $9,840.00 " , # 9840 = 80 * 123
2021-05-14 16:48:00 +02:00
f " Billing email: <strong> { user . email } </strong> " ,
2021-02-12 08:20:45 +01:00
" Billed by invoice " ,
2020-12-23 17:08:27 +01:00
" You can only increase the number of licenses. " ,
" Number of licenses " ,
" Licenses in next renewal " ,
2021-02-12 08:19:30 +01:00
] :
2018-12-23 09:10:57 +01:00
self . assert_in_response ( substring , response )
2018-11-05 22:37:22 +01:00
2020-04-23 20:10:15 +02:00
@mock_stripe ( tested_timestamp_fields = [ " created " ] )
def test_free_trial_upgrade_by_card ( self , * mocks : Mock ) - > None :
user = self . example_user ( " hamlet " )
self . login_user ( user )
2020-05-14 18:21:23 +02:00
with self . settings ( FREE_TRIAL_DAYS = 60 ) :
2020-04-23 20:10:15 +02:00
response = self . client_get ( " /upgrade/ " )
2020-05-14 18:21:23 +02:00
free_trial_end_date = self . now + timedelta ( days = 60 )
2020-04-23 20:10:15 +02:00
2021-02-12 08:20:45 +01:00
self . assert_in_success_response ( [ " Pay annually " , " Free Trial " , " 60 day " ] , response )
2020-04-23 20:10:15 +02:00
self . assertNotEqual ( user . realm . plan_type , Realm . STANDARD )
self . assertFalse ( Customer . objects . filter ( realm = user . realm ) . exists ( ) )
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2020-04-23 20:10:15 +02:00
self . upgrade ( )
2021-02-12 08:19:30 +01:00
stripe_customer = stripe_get_customer (
Customer . objects . get ( realm = user . realm ) . stripe_customer_id
)
2021-02-12 08:20:45 +01:00
self . assertEqual ( stripe_customer . default_source . id [ : 5 ] , " card_ " )
2020-04-23 20:10:15 +02:00
self . assertEqual ( stripe_customer . description , " zulip (Zulip Dev) " )
self . assertEqual ( stripe_customer . discount , None )
self . assertEqual ( stripe_customer . email , user . email )
metadata_dict = dict ( stripe_customer . metadata )
2021-02-12 08:20:45 +01:00
self . assertEqual ( metadata_dict [ " realm_str " ] , " zulip " )
2020-04-23 20:10:15 +02:00
try :
2021-02-12 08:20:45 +01:00
int ( metadata_dict [ " realm_id " ] )
2020-04-23 20:10:15 +02:00
except ValueError : # nocoverage
raise AssertionError ( " realm_id is not a number " )
2020-09-02 07:55:39 +02:00
self . assertFalse ( stripe . Charge . list ( customer = stripe_customer . id ) )
2020-04-23 20:10:15 +02:00
2020-09-02 07:55:39 +02:00
self . assertFalse ( stripe . Invoice . list ( customer = stripe_customer . id ) )
2020-04-23 20:10:15 +02:00
customer = Customer . objects . get ( stripe_customer_id = stripe_customer . id , realm = user . realm )
plan = CustomerPlan . objects . get (
2021-02-12 08:19:30 +01:00
customer = customer ,
automanage_licenses = True ,
price_per_license = 8000 ,
fixed_price = None ,
discount = None ,
billing_cycle_anchor = self . now ,
billing_schedule = CustomerPlan . ANNUAL ,
invoiced_through = LicenseLedger . objects . first ( ) ,
next_invoice_date = free_trial_end_date ,
tier = CustomerPlan . STANDARD ,
status = CustomerPlan . FREE_TRIAL ,
)
2020-04-23 20:10:15 +02:00
LicenseLedger . objects . get (
2021-02-12 08:19:30 +01:00
plan = plan ,
is_renewal = True ,
event_time = self . now ,
licenses = self . seat_count ,
licenses_at_next_renewal = self . seat_count ,
)
audit_log_entries = list (
RealmAuditLog . objects . filter ( acting_user = user )
2021-02-12 08:20:45 +01:00
. values_list ( " event_type " , " event_time " )
. order_by ( " id " )
2021-02-12 08:19:30 +01:00
)
self . assertEqual (
2020-12-04 10:54:15 +01:00
audit_log_entries [ : 3 ] ,
2021-02-12 08:19:30 +01:00
[
(
RealmAuditLog . STRIPE_CUSTOMER_CREATED ,
timestamp_to_datetime ( stripe_customer . created ) ,
) ,
(
RealmAuditLog . STRIPE_CARD_CHANGED ,
timestamp_to_datetime ( stripe_customer . created ) ,
) ,
( RealmAuditLog . CUSTOMER_PLAN_CREATED , self . now ) ,
] ,
)
2020-12-04 10:54:15 +01:00
self . assertEqual ( audit_log_entries [ 3 ] [ 0 ] , RealmAuditLog . REALM_PLAN_TYPE_CHANGED )
2021-02-12 08:19:30 +01:00
self . assertEqual (
orjson . loads (
RealmAuditLog . objects . filter ( event_type = RealmAuditLog . CUSTOMER_PLAN_CREATED )
2021-02-12 08:20:45 +01:00
. values_list ( " extra_data " , flat = True )
2021-02-12 08:19:30 +01:00
. first ( )
2021-02-12 08:20:45 +01:00
) [ " automanage_licenses " ] ,
2021-02-12 08:19:30 +01:00
True ,
)
2020-04-23 20:10:15 +02:00
realm = get_realm ( " zulip " )
self . assertEqual ( realm . plan_type , Realm . STANDARD )
self . assertEqual ( realm . max_invites , Realm . INVITES_STANDARD_REALM_DAILY_MAX )
2021-02-12 08:20:45 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
2020-04-23 20:10:15 +02:00
response = self . client_get ( " /billing/ " )
2021-02-12 08:20:45 +01:00
self . assert_not_in_success_response ( [ " Pay annually " ] , response )
2020-04-23 20:10:15 +02:00
for substring in [
2021-02-12 08:20:45 +01:00
" Zulip Standard " ,
" Free Trial " ,
2021-02-12 08:19:30 +01:00
str ( self . seat_count ) ,
2021-02-12 08:20:45 +01:00
" You are using " ,
f " { self . seat_count } of { self . seat_count } licenses " ,
" Your plan will be upgraded to " ,
" March 2, 2012 " ,
f " $ { 80 * self . seat_count } .00 " ,
2021-05-14 16:48:00 +02:00
f " Billing email: <strong> { user . email } </strong> " ,
2021-02-12 08:20:45 +01:00
" Visa ending in 4242 " ,
" Update card " ,
2021-02-12 08:19:30 +01:00
] :
2020-04-23 20:10:15 +02:00
self . assert_in_response ( substring , response )
2020-05-22 15:42:46 +02:00
self . assert_not_in_success_response ( [ " Go to your Zulip organization " ] , response )
2021-02-12 08:20:45 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
2020-09-13 00:11:30 +02:00
response = self . client_get ( " /billing/ " , { " onboarding " : " true " } )
2020-05-22 15:42:46 +02:00
self . assert_in_success_response ( [ " Go to your Zulip organization " ] , response )
2020-04-23 20:10:15 +02:00
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 12 ) :
2020-04-23 20:10:15 +02:00
update_license_ledger_if_needed ( realm , self . now )
self . assertEqual (
2021-02-12 08:20:45 +01:00
LicenseLedger . objects . order_by ( " -id " )
. values_list ( " licenses " , " licenses_at_next_renewal " )
2021-02-12 08:19:30 +01:00
. first ( ) ,
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
( 12 , 12 ) ,
2020-04-23 20:10:15 +02:00
)
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 15 ) :
2020-04-23 20:10:15 +02:00
update_license_ledger_if_needed ( realm , self . next_month )
self . assertEqual (
2021-02-12 08:20:45 +01:00
LicenseLedger . objects . order_by ( " -id " )
. values_list ( " licenses " , " licenses_at_next_renewal " )
2021-02-12 08:19:30 +01:00
. first ( ) ,
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
( 15 , 15 ) ,
2020-04-23 20:10:15 +02:00
)
invoice_plans_as_needed ( self . next_month )
2020-09-02 07:55:39 +02:00
self . assertFalse ( stripe . Invoice . list ( customer = stripe_customer . id ) )
2020-04-23 20:10:15 +02:00
customer_plan = CustomerPlan . objects . get ( customer = customer )
self . assertEqual ( customer_plan . status , CustomerPlan . FREE_TRIAL )
2020-05-14 18:21:23 +02:00
self . assertEqual ( customer_plan . next_invoice_date , free_trial_end_date )
2020-04-23 20:10:15 +02:00
2020-05-14 18:21:23 +02:00
invoice_plans_as_needed ( free_trial_end_date )
2020-04-23 20:10:15 +02:00
customer_plan . refresh_from_db ( )
realm . refresh_from_db ( )
self . assertEqual ( customer_plan . status , CustomerPlan . ACTIVE )
2020-05-14 18:21:23 +02:00
self . assertEqual ( customer_plan . next_invoice_date , add_months ( free_trial_end_date , 1 ) )
2020-04-23 20:10:15 +02:00
self . assertEqual ( realm . plan_type , Realm . STANDARD )
2020-09-02 07:55:39 +02:00
[ invoice ] = stripe . Invoice . list ( customer = stripe_customer . id )
2020-04-23 20:10:15 +02:00
invoice_params = {
2021-02-12 08:19:30 +01:00
" amount_due " : 15 * 80 * 100 ,
" amount_paid " : 0 ,
" amount_remaining " : 15 * 80 * 100 ,
" auto_advance " : True ,
" billing " : " charge_automatically " ,
" collection_method " : " charge_automatically " ,
" customer_email " : self . example_email ( " hamlet " ) ,
" discount " : None ,
" paid " : False ,
" status " : " open " ,
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
" total " : 15 * 80 * 100 ,
2020-04-23 20:10:15 +02:00
}
for key , value in invoice_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( invoice . get ( key ) , value )
[ invoice_item ] = invoice . get ( " lines " )
2020-04-23 20:10:15 +02:00
invoice_item_params = {
2021-02-12 08:19:30 +01:00
" amount " : 15 * 80 * 100 ,
" description " : " Zulip Standard - renewal " ,
" plan " : None ,
" quantity " : 15 ,
" subscription " : None ,
" discountable " : False ,
2020-04-23 20:10:15 +02:00
" period " : {
2020-05-14 18:21:23 +02:00
" start " : datetime_to_timestamp ( free_trial_end_date ) ,
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
" end " : datetime_to_timestamp ( add_months ( free_trial_end_date , 12 ) ) ,
2020-04-23 20:10:15 +02:00
} ,
}
for key , value in invoice_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( invoice_item [ key ] , value )
2020-04-23 20:10:15 +02:00
2020-05-14 18:21:23 +02:00
invoice_plans_as_needed ( add_months ( free_trial_end_date , 1 ) )
2020-09-02 07:55:39 +02:00
[ invoice ] = stripe . Invoice . list ( customer = stripe_customer . id )
2020-04-23 20:10:15 +02:00
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 19 ) :
2020-05-14 18:21:23 +02:00
update_license_ledger_if_needed ( realm , add_months ( free_trial_end_date , 10 ) )
2020-04-23 20:10:15 +02:00
self . assertEqual (
2021-02-12 08:20:45 +01:00
LicenseLedger . objects . order_by ( " -id " )
. values_list ( " licenses " , " licenses_at_next_renewal " )
2021-02-12 08:19:30 +01:00
. first ( ) ,
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
( 19 , 19 ) ,
2020-04-23 20:10:15 +02:00
)
2020-05-14 18:21:23 +02:00
invoice_plans_as_needed ( add_months ( free_trial_end_date , 10 ) )
2020-09-02 07:55:39 +02:00
[ invoice0 , invoice1 ] = stripe . Invoice . list ( customer = stripe_customer . id )
2020-04-23 20:10:15 +02:00
invoice_params = {
2021-02-12 08:19:30 +01:00
" amount_due " : 5172 ,
" auto_advance " : True ,
" billing " : " charge_automatically " ,
" collection_method " : " charge_automatically " ,
" customer_email " : " hamlet@zulip.com " ,
2020-04-23 20:10:15 +02:00
}
2020-09-02 07:55:39 +02:00
[ invoice_item ] = invoice0 . get ( " lines " )
2020-04-23 20:10:15 +02:00
invoice_item_params = {
2021-02-12 08:19:30 +01:00
" amount " : 5172 ,
" description " : " Additional license (Jan 2, 2013 - Mar 2, 2013) " ,
" discountable " : False ,
" quantity " : 4 ,
2020-04-23 20:10:15 +02:00
" period " : {
2020-05-14 18:21:23 +02:00
" start " : datetime_to_timestamp ( add_months ( free_trial_end_date , 10 ) ) ,
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
" end " : datetime_to_timestamp ( add_months ( free_trial_end_date , 12 ) ) ,
} ,
2020-04-23 20:10:15 +02:00
}
2020-05-14 18:21:23 +02:00
invoice_plans_as_needed ( add_months ( free_trial_end_date , 12 ) )
2020-09-02 07:55:39 +02:00
[ invoice0 , invoice1 , invoice2 ] = stripe . Invoice . list ( customer = stripe_customer . id )
2020-04-23 20:10:15 +02:00
@mock_stripe ( tested_timestamp_fields = [ " created " ] )
def test_free_trial_upgrade_by_invoice ( self , * mocks : Mock ) - > None :
user = self . example_user ( " hamlet " )
self . login_user ( user )
2020-05-14 18:21:23 +02:00
free_trial_end_date = self . now + timedelta ( days = 60 )
with self . settings ( FREE_TRIAL_DAYS = 60 ) :
2020-04-23 20:10:15 +02:00
response = self . client_get ( " /upgrade/ " )
2021-02-12 08:20:45 +01:00
self . assert_in_success_response ( [ " Pay annually " , " Free Trial " , " 60 day " ] , response )
2020-04-23 20:10:15 +02:00
self . assertNotEqual ( user . realm . plan_type , Realm . STANDARD )
self . assertFalse ( Customer . objects . filter ( realm = user . realm ) . exists ( ) )
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2020-04-23 20:10:15 +02:00
self . upgrade ( invoice = True )
2021-02-12 08:19:30 +01:00
stripe_customer = stripe_get_customer (
Customer . objects . get ( realm = user . realm ) . stripe_customer_id
)
2020-04-23 20:10:15 +02:00
self . assertEqual ( stripe_customer . discount , None )
self . assertEqual ( stripe_customer . email , user . email )
metadata_dict = dict ( stripe_customer . metadata )
2021-02-12 08:20:45 +01:00
self . assertEqual ( metadata_dict [ " realm_str " ] , " zulip " )
2020-04-23 20:10:15 +02:00
try :
2021-02-12 08:20:45 +01:00
int ( metadata_dict [ " realm_id " ] )
2020-04-23 20:10:15 +02:00
except ValueError : # nocoverage
raise AssertionError ( " realm_id is not a number " )
2020-09-02 07:55:39 +02:00
self . assertFalse ( stripe . Invoice . list ( customer = stripe_customer . id ) )
2020-04-23 20:10:15 +02:00
customer = Customer . objects . get ( stripe_customer_id = stripe_customer . id , realm = user . realm )
plan = CustomerPlan . objects . get (
2021-02-12 08:19:30 +01:00
customer = customer ,
automanage_licenses = False ,
price_per_license = 8000 ,
fixed_price = None ,
discount = None ,
billing_cycle_anchor = self . now ,
billing_schedule = CustomerPlan . ANNUAL ,
invoiced_through = LicenseLedger . objects . first ( ) ,
next_invoice_date = free_trial_end_date ,
tier = CustomerPlan . STANDARD ,
status = CustomerPlan . FREE_TRIAL ,
)
2020-04-23 20:10:15 +02:00
LicenseLedger . objects . get (
2021-02-12 08:19:30 +01:00
plan = plan ,
is_renewal = True ,
event_time = self . now ,
licenses = 123 ,
licenses_at_next_renewal = 123 ,
)
audit_log_entries = list (
RealmAuditLog . objects . filter ( acting_user = user )
2021-02-12 08:20:45 +01:00
. values_list ( " event_type " , " event_time " )
. order_by ( " id " )
2021-02-12 08:19:30 +01:00
)
self . assertEqual (
2020-12-04 10:54:15 +01:00
audit_log_entries [ : 2 ] ,
2021-02-12 08:19:30 +01:00
[
(
RealmAuditLog . STRIPE_CUSTOMER_CREATED ,
timestamp_to_datetime ( stripe_customer . created ) ,
) ,
( RealmAuditLog . CUSTOMER_PLAN_CREATED , self . now ) ,
] ,
)
2020-12-04 10:54:15 +01:00
self . assertEqual ( audit_log_entries [ 2 ] [ 0 ] , RealmAuditLog . REALM_PLAN_TYPE_CHANGED )
2021-02-12 08:19:30 +01:00
self . assertEqual (
orjson . loads (
RealmAuditLog . objects . filter ( event_type = RealmAuditLog . CUSTOMER_PLAN_CREATED )
2021-02-12 08:20:45 +01:00
. values_list ( " extra_data " , flat = True )
2021-02-12 08:19:30 +01:00
. first ( )
2021-02-12 08:20:45 +01:00
) [ " automanage_licenses " ] ,
2021-02-12 08:19:30 +01:00
False ,
)
2020-04-23 20:10:15 +02:00
realm = get_realm ( " zulip " )
self . assertEqual ( realm . plan_type , Realm . STANDARD )
self . assertEqual ( realm . max_invites , Realm . INVITES_STANDARD_REALM_DAILY_MAX )
2021-02-12 08:20:45 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
2020-04-23 20:10:15 +02:00
response = self . client_get ( " /billing/ " )
2021-02-12 08:20:45 +01:00
self . assert_not_in_success_response ( [ " Pay annually " ] , response )
2020-04-23 20:10:15 +02:00
for substring in [
2021-02-12 08:20:45 +01:00
" Zulip Standard " ,
" Free Trial " ,
2021-02-12 08:19:30 +01:00
str ( self . seat_count ) ,
2021-02-12 08:20:45 +01:00
" You are using " ,
f " { self . seat_count } of { 123 } licenses " ,
" Your plan will be upgraded to " ,
" March 2, 2012 " ,
f " { 80 * 123 : ,.2f } " ,
2021-05-14 16:48:00 +02:00
f " Billing email: <strong> { user . email } </strong> " ,
2021-02-12 08:20:45 +01:00
" Billed by invoice " ,
2020-04-23 20:10:15 +02:00
] :
self . assert_in_response ( substring , response )
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.invoice_plan " ) as mocked :
2020-04-23 20:10:15 +02:00
invoice_plans_as_needed ( self . next_month )
mocked . assert_not_called ( )
mocked . reset_mock ( )
customer_plan = CustomerPlan . objects . get ( customer = customer )
self . assertEqual ( customer_plan . status , CustomerPlan . FREE_TRIAL )
2020-05-14 18:21:23 +02:00
self . assertEqual ( customer_plan . next_invoice_date , free_trial_end_date )
2020-04-23 20:10:15 +02:00
2020-05-14 18:21:23 +02:00
invoice_plans_as_needed ( free_trial_end_date )
2020-04-23 20:10:15 +02:00
customer_plan . refresh_from_db ( )
realm . refresh_from_db ( )
self . assertEqual ( customer_plan . status , CustomerPlan . ACTIVE )
2020-05-14 18:21:23 +02:00
self . assertEqual ( customer_plan . next_invoice_date , add_months ( free_trial_end_date , 12 ) )
2020-04-23 20:10:15 +02:00
self . assertEqual ( realm . plan_type , Realm . STANDARD )
2020-09-02 07:55:39 +02:00
[ invoice ] = stripe . Invoice . list ( customer = stripe_customer . id )
2020-04-23 20:10:15 +02:00
invoice_params = {
2021-02-12 08:19:30 +01:00
" amount_due " : 123 * 80 * 100 ,
" amount_paid " : 0 ,
" amount_remaining " : 123 * 80 * 100 ,
" auto_advance " : True ,
" billing " : " send_invoice " ,
" collection_method " : " send_invoice " ,
" customer_email " : self . example_email ( " hamlet " ) ,
" discount " : None ,
" paid " : False ,
" status " : " open " ,
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
" total " : 123 * 80 * 100 ,
2020-04-23 20:10:15 +02:00
}
for key , value in invoice_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( invoice . get ( key ) , value )
[ invoice_item ] = invoice . get ( " lines " )
2020-04-23 20:10:15 +02:00
invoice_item_params = {
2021-02-12 08:19:30 +01:00
" amount " : 123 * 80 * 100 ,
" description " : " Zulip Standard - renewal " ,
" plan " : None ,
" quantity " : 123 ,
" subscription " : None ,
" discountable " : False ,
2020-04-23 20:10:15 +02:00
" period " : {
2020-05-14 18:21:23 +02:00
" start " : datetime_to_timestamp ( free_trial_end_date ) ,
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
" end " : datetime_to_timestamp ( add_months ( free_trial_end_date , 12 ) ) ,
2020-04-23 20:10:15 +02:00
} ,
}
for key , value in invoice_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( invoice_item [ key ] , value )
2020-04-23 20:10:15 +02:00
2020-05-14 18:21:23 +02:00
invoice_plans_as_needed ( add_months ( free_trial_end_date , 1 ) )
2020-09-02 07:55:39 +02:00
[ invoice ] = stripe . Invoice . list ( customer = stripe_customer . id )
2020-04-23 20:10:15 +02:00
2020-05-14 18:21:23 +02:00
invoice_plans_as_needed ( add_months ( free_trial_end_date , 10 ) )
2020-09-02 07:55:39 +02:00
[ invoice ] = stripe . Invoice . list ( customer = stripe_customer . id )
2020-04-23 20:10:15 +02:00
2020-05-14 18:21:23 +02:00
invoice_plans_as_needed ( add_months ( free_trial_end_date , 12 ) )
2020-09-02 07:55:39 +02:00
[ invoice0 , invoice1 ] = stripe . Invoice . list ( customer = stripe_customer . id )
2020-04-23 20:10:15 +02:00
2018-12-03 19:23:13 +01:00
@mock_stripe ( )
2018-12-02 19:05:34 +01:00
def test_billing_page_permissions ( self , * mocks : Mock ) - > None :
2020-07-15 22:18:32 +02:00
# Guest users can't access /upgrade page
2021-02-12 08:20:45 +01:00
self . login_user ( self . example_user ( " polonius " ) )
2020-07-15 22:18:32 +02:00
response = self . client_get ( " /upgrade/ " , follow = True )
self . assertEqual ( response . status_code , 404 )
2018-07-11 16:36:52 +02:00
# Check that non-admins can access /upgrade via /billing, when there is no Customer object
2021-02-12 08:20:45 +01:00
self . login_user ( self . example_user ( " hamlet " ) )
2018-07-11 16:36:52 +02:00
response = self . client_get ( " /billing/ " )
self . assertEqual ( response . status_code , 302 )
2021-02-12 08:20:45 +01:00
self . assertEqual ( " /upgrade/ " , response . url )
2018-07-11 16:36:52 +02:00
# Check that non-admins can sign up and pay
2018-11-29 03:15:27 +01:00
self . upgrade ( )
2018-07-11 16:36:52 +02:00
# Check that the non-admin hamlet can still access /billing
response = self . client_get ( " /billing/ " )
2020-07-14 14:40:39 +02:00
self . assert_in_success_response ( [ " Your current plan is " ] , response )
# Check realm owners can access billing, even though they are not a billing admin
2021-02-12 08:20:45 +01:00
desdemona = self . example_user ( " desdemona " )
2020-07-14 14:40:39 +02:00
desdemona . role = UserProfile . ROLE_REALM_OWNER
desdemona . save ( update_fields = [ " role " ] )
2021-02-12 08:20:45 +01:00
self . login_user ( self . example_user ( " desdemona " ) )
2018-07-11 16:36:52 +02:00
response = self . client_get ( " /billing/ " )
2020-07-14 14:40:39 +02:00
self . assert_in_success_response ( [ " Your current plan is " ] , response )
# Check that member who is not a billing admin does not have access
2021-02-12 08:20:45 +01:00
self . login_user ( self . example_user ( " cordelia " ) )
2018-07-11 16:36:52 +02:00
response = self . client_get ( " /billing/ " )
2021-02-12 08:19:30 +01:00
self . assert_in_success_response (
[ " You must be an organization owner or a billing administrator " ] , response
)
2018-07-11 16:36:52 +02:00
2018-12-04 00:29:23 +01:00
@mock_stripe ( tested_timestamp_fields = [ " created " ] )
2018-12-15 09:33:25 +01:00
def test_upgrade_by_card_with_outdated_seat_count ( self , * mocks : Mock ) - > None :
2021-02-12 08:20:45 +01:00
hamlet = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( hamlet )
2018-12-15 09:33:25 +01:00
new_seat_count = 23
2018-06-28 00:48:51 +02:00
# Change the seat count while the user is going through the upgrade flow
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = new_seat_count ) :
2018-11-29 03:15:27 +01:00
self . upgrade ( )
2018-12-15 09:33:25 +01:00
stripe_customer_id = Customer . objects . first ( ) . stripe_customer_id
# Check that the Charge used the old quantity, not new_seat_count
2020-09-02 07:55:39 +02:00
[ charge ] = stripe . Charge . list ( customer = stripe_customer_id )
self . assertEqual ( 8000 * self . seat_count , charge . amount )
2018-12-15 09:33:25 +01:00
# Check that the invoice has a credit for the old amount and a charge for the new one
2020-09-02 07:55:39 +02:00
[ stripe_invoice ] = stripe . Invoice . list ( customer = stripe_customer_id )
2021-02-12 08:19:30 +01:00
self . assertEqual (
[ 8000 * new_seat_count , - 8000 * self . seat_count ] ,
[ item . amount for item in stripe_invoice . lines ] ,
)
2018-12-28 07:20:30 +01:00
# Check LicenseLedger has the new amount
self . assertEqual ( LicenseLedger . objects . first ( ) . licenses , new_seat_count )
self . assertEqual ( LicenseLedger . objects . first ( ) . licenses_at_next_renewal , new_seat_count )
2018-06-28 00:48:51 +02:00
2018-12-03 19:23:13 +01:00
@mock_stripe ( )
2018-12-15 09:33:25 +01:00
def test_upgrade_where_first_card_fails ( self , * mocks : Mock ) - > None :
2018-08-14 03:33:31 +02:00
user = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( user )
2018-10-18 20:04:45 +02:00
# From https://stripe.com/docs/testing#cards: Attaching this card to
# a Customer object succeeds, but attempts to charge the customer fail.
2021-02-12 08:20:45 +01:00
with self . assertLogs ( " corporate.stripe " , " INFO " ) as m :
self . upgrade ( stripe_token = stripe_create_token ( " 4000000000000341 " ) . id )
2021-02-12 08:19:30 +01:00
self . assertEqual (
m . output ,
2021-02-12 08:20:45 +01:00
[ " INFO:corporate.stripe:Stripe card error: 402 card_error card_declined None " ] ,
2021-02-12 08:19:30 +01:00
)
2018-12-15 09:33:25 +01:00
# Check that we created a Customer object but no CustomerPlan
2021-02-12 08:20:45 +01:00
stripe_customer_id = Customer . objects . get ( realm = get_realm ( " zulip " ) ) . stripe_customer_id
2018-12-15 09:33:25 +01:00
self . assertFalse ( CustomerPlan . objects . exists ( ) )
# Check that we created a Customer in stripe, a failed Charge, and no Invoices or Invoice Items
self . assertTrue ( stripe_get_customer ( stripe_customer_id ) )
2020-09-02 07:55:39 +02:00
[ charge ] = stripe . Charge . list ( customer = stripe_customer_id )
2021-02-12 08:20:45 +01:00
self . assertEqual ( charge . failure_code , " card_declined " )
2018-12-15 09:33:25 +01:00
# TODO: figure out what these actually are
self . assertFalse ( stripe . Invoice . list ( customer = stripe_customer_id ) )
self . assertFalse ( stripe . InvoiceItem . list ( customer = stripe_customer_id ) )
2018-08-14 03:33:31 +02:00
# Check that we correctly populated RealmAuditLog
2021-02-12 08:19:30 +01:00
audit_log_entries = list (
RealmAuditLog . objects . filter ( acting_user = user )
2021-02-12 08:20:45 +01:00
. values_list ( " event_type " , flat = True )
. order_by ( " id " )
2021-02-12 08:19:30 +01:00
)
self . assertEqual (
audit_log_entries ,
[ RealmAuditLog . STRIPE_CUSTOMER_CREATED , RealmAuditLog . STRIPE_CARD_CHANGED ] ,
)
2018-08-14 03:33:31 +02:00
# Check that we did not update Realm
realm = get_realm ( " zulip " )
2018-12-15 09:33:25 +01:00
self . assertNotEqual ( realm . plan_type , Realm . STANDARD )
2018-08-14 03:33:31 +02:00
# Check that we still get redirected to /upgrade
response = self . client_get ( " /billing/ " )
self . assertEqual ( response . status_code , 302 )
2021-02-12 08:20:45 +01:00
self . assertEqual ( " /upgrade/ " , response . url )
2018-08-14 03:33:31 +02:00
2018-12-15 09:33:25 +01:00
# Try again, with a valid card, after they added a few users
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 23 ) :
with patch ( " corporate.views.get_latest_seat_count " , return_value = 23 ) :
2018-12-15 09:33:25 +01:00
self . upgrade ( )
2021-02-12 08:20:45 +01:00
customer = Customer . objects . get ( realm = get_realm ( " zulip " ) )
2018-12-15 09:33:25 +01:00
# It's impossible to create two Customers, but check that we didn't
# change stripe_customer_id
self . assertEqual ( customer . stripe_customer_id , stripe_customer_id )
2018-12-28 07:20:30 +01:00
# Check that we successfully added a CustomerPlan, and have the right number of licenses
plan = CustomerPlan . objects . get ( customer = customer )
ledger_entry = LicenseLedger . objects . get ( plan = plan )
self . assertEqual ( ledger_entry . licenses , 23 )
self . assertEqual ( ledger_entry . licenses_at_next_renewal , 23 )
2018-12-15 09:33:25 +01:00
# Check the Charges and Invoices in Stripe
2020-09-02 07:55:39 +02:00
[ charge0 , charge1 ] = stripe . Charge . list ( customer = stripe_customer_id )
self . assertEqual ( 8000 * 23 , charge0 . amount )
[ stripe_invoice ] = stripe . Invoice . list ( customer = stripe_customer_id )
2021-02-12 08:19:30 +01:00
self . assertEqual ( [ 8000 * 23 , - 8000 * 23 ] , [ item . amount for item in stripe_invoice . lines ] )
2018-08-14 03:33:31 +02:00
# Check that we correctly populated RealmAuditLog
2021-02-12 08:19:30 +01:00
audit_log_entries = list (
RealmAuditLog . objects . filter ( acting_user = user )
2021-02-12 08:20:45 +01:00
. values_list ( " event_type " , flat = True )
. order_by ( " id " )
2021-02-12 08:19:30 +01:00
)
self . assertEqual (
audit_log_entries ,
[
RealmAuditLog . STRIPE_CUSTOMER_CREATED ,
RealmAuditLog . STRIPE_CARD_CHANGED ,
RealmAuditLog . STRIPE_CARD_CHANGED ,
RealmAuditLog . CUSTOMER_PLAN_CREATED ,
2020-12-04 10:54:15 +01:00
RealmAuditLog . REALM_PLAN_TYPE_CHANGED ,
2021-02-12 08:19:30 +01:00
] ,
)
2018-08-14 03:33:31 +02:00
# Check that we correctly updated Realm
realm = get_realm ( " zulip " )
2018-12-15 09:33:25 +01:00
self . assertEqual ( realm . plan_type , Realm . STANDARD )
2018-08-14 03:33:31 +02:00
# Check that we can no longer access /upgrade
response = self . client_get ( " /upgrade/ " )
self . assertEqual ( response . status_code , 302 )
2021-02-12 08:20:45 +01:00
self . assertEqual ( " /billing/ " , response . url )
2018-08-14 03:33:31 +02:00
2018-07-13 13:33:05 +02:00
def test_upgrade_with_tampered_seat_count ( self ) - > None :
2021-02-12 08:20:45 +01:00
hamlet = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( hamlet )
2021-02-12 08:20:45 +01:00
response = self . upgrade ( talk_to_stripe = False , salt = " badsalt " )
2018-12-07 18:43:22 +01:00
self . assert_json_error_contains ( response , " Something went wrong. Please contact " )
2021-02-12 08:20:45 +01:00
self . assertEqual ( orjson . loads ( response . content ) [ " error_description " ] , " tampered seat count " )
2018-07-13 13:33:05 +02:00
2019-01-29 16:01:31 +01:00
def test_upgrade_race_condition ( self ) - > None :
2021-02-12 08:20:45 +01:00
hamlet = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( hamlet )
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
with self . assertLogs ( " corporate.stripe " , " WARNING " ) as m :
2019-01-29 16:01:31 +01:00
with self . assertRaises ( BillingError ) as context :
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
self . assertEqual ( " subscribing with existing subscription " , context . exception . description )
2021-02-04 11:20:53 +01:00
self . assertEqual (
2021-02-12 08:19:30 +01:00
m . output [ 0 ] ,
2021-02-04 11:20:53 +01:00
f " WARNING:corporate.stripe:Customer <Customer <Realm: zulip { hamlet . realm . id } > id> trying to upgrade, but has an active subscription " ,
2021-02-12 08:19:30 +01:00
)
2021-05-17 05:41:32 +02:00
self . assert_length ( m . output , 1 )
2019-01-29 16:01:31 +01:00
2018-12-22 05:29:25 +01:00
def test_check_upgrade_parameters ( self ) - > None :
# Tests all the error paths except 'not enough licenses'
2021-02-12 08:19:30 +01:00
def check_error (
2021-04-09 12:43:44 +02:00
error_message : str ,
error_description : str ,
upgrade_params : Mapping [ str , Any ] ,
del_args : Sequence [ str ] = [ ] ,
2021-02-12 08:19:30 +01:00
) - > None :
2018-12-22 05:29:25 +01:00
response = self . upgrade ( talk_to_stripe = False , del_args = del_args , * * upgrade_params )
2021-04-09 12:43:44 +02:00
self . assert_json_error_contains ( response , error_message )
if error_description :
self . assertEqual (
orjson . loads ( response . content ) [ " error_description " ] , error_description
)
2018-12-22 05:29:25 +01:00
2021-02-12 08:20:45 +01:00
hamlet = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( hamlet )
2021-04-09 12:43:44 +02:00
check_error ( " Invalid billing_modality " , " " , { " billing_modality " : " invalid " } )
check_error ( " Invalid schedule " , " " , { " schedule " : " invalid " } )
check_error ( " Invalid license_management " , " " , { " license_management " : " invalid " } )
check_error (
" Something went wrong. Please contact " ,
" autopay with no card " ,
{ } ,
del_args = [ " stripe_token " ] ,
)
2018-12-22 05:29:25 +01:00
def test_upgrade_license_counts ( self ) - > None :
2021-02-12 08:19:30 +01:00
def check_min_licenses_error (
invoice : bool ,
licenses : Optional [ int ] ,
min_licenses_in_response : int ,
upgrade_params : Dict [ str , Any ] = { } ,
) - > None :
2018-12-22 05:29:25 +01:00
if licenses is None :
2021-02-12 08:20:45 +01:00
del_args = [ " licenses " ]
2018-12-22 05:29:25 +01:00
else :
del_args = [ ]
2021-02-12 08:20:45 +01:00
upgrade_params [ " licenses " ] = licenses
2021-02-12 08:19:30 +01:00
response = self . upgrade (
invoice = invoice , talk_to_stripe = False , del_args = del_args , * * upgrade_params
)
2020-06-09 00:25:09 +02:00
self . assert_json_error_contains ( response , f " at least { min_licenses_in_response } users " )
2021-02-12 08:19:30 +01:00
self . assertEqual (
2021-02-12 08:20:45 +01:00
orjson . loads ( response . content ) [ " error_description " ] , " not enough licenses "
2021-02-12 08:19:30 +01:00
)
2018-12-22 05:29:25 +01:00
2020-05-08 12:43:52 +02:00
def check_max_licenses_error ( licenses : int ) - > None :
2021-02-12 08:19:30 +01:00
response = self . upgrade ( invoice = True , talk_to_stripe = False , licenses = licenses )
self . assert_json_error_contains (
response , f " with more than { MAX_INVOICED_LICENSES } licenses "
)
self . assertEqual (
2021-02-12 08:20:45 +01:00
orjson . loads ( response . content ) [ " error_description " ] , " too many licenses "
2021-02-12 08:19:30 +01:00
)
2020-05-08 12:43:52 +02:00
2021-02-12 08:19:30 +01:00
def check_success (
invoice : bool , licenses : Optional [ int ] , upgrade_params : Dict [ str , Any ] = { }
) - > None :
2018-12-22 05:29:25 +01:00
if licenses is None :
2021-02-12 08:20:45 +01:00
del_args = [ " licenses " ]
2018-12-22 05:29:25 +01:00
else :
del_args = [ ]
2021-02-12 08:20:45 +01:00
upgrade_params [ " licenses " ] = licenses
with patch ( " corporate.views.process_initial_upgrade " ) :
2021-02-12 08:19:30 +01:00
response = self . upgrade (
invoice = invoice , talk_to_stripe = False , del_args = del_args , * * upgrade_params
)
2018-12-22 05:29:25 +01:00
self . assert_json_success ( response )
2018-07-22 17:23:57 +02:00
2021-02-12 08:20:45 +01:00
hamlet = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( hamlet )
2018-12-22 05:29:25 +01:00
# Autopay with licenses < seat count
2021-02-12 08:19:30 +01:00
check_min_licenses_error (
2021-02-12 08:20:45 +01:00
False , self . seat_count - 1 , self . seat_count , { " license_management " : " manual " }
2021-02-12 08:19:30 +01:00
)
2018-12-22 05:29:25 +01:00
# Autopay with not setting licenses
2021-02-12 08:20:45 +01:00
check_min_licenses_error ( False , None , self . seat_count , { " license_management " : " manual " } )
2018-12-22 05:29:25 +01:00
# Invoice with licenses < MIN_INVOICED_LICENSES
2020-05-08 12:43:52 +02:00
check_min_licenses_error ( True , MIN_INVOICED_LICENSES - 1 , MIN_INVOICED_LICENSES )
2018-12-22 05:29:25 +01:00
# Invoice with licenses < seat count
2020-12-17 16:33:19 +01:00
with patch ( " corporate.lib.stripe.MIN_INVOICED_LICENSES " , 3 ) :
2020-05-08 12:43:52 +02:00
check_min_licenses_error ( True , 4 , self . seat_count )
2018-12-22 05:29:25 +01:00
# Invoice with not setting licenses
2020-05-08 12:43:52 +02:00
check_min_licenses_error ( True , None , MIN_INVOICED_LICENSES )
# Invoice exceeding max licenses
check_max_licenses_error ( MAX_INVOICED_LICENSES + 1 )
2021-02-12 08:19:30 +01:00
with patch (
" corporate.lib.stripe.get_latest_seat_count " , return_value = MAX_INVOICED_LICENSES + 5
) :
2020-05-08 12:43:52 +02:00
check_max_licenses_error ( MAX_INVOICED_LICENSES + 5 )
2018-12-22 05:29:25 +01:00
# Autopay with automatic license_management
check_success ( False , None )
# Autopay with automatic license_management, should just ignore the licenses entry
check_success ( False , self . seat_count )
# Autopay
2021-02-12 08:20:45 +01:00
check_success ( False , self . seat_count , { " license_management " : " manual " } )
2020-05-08 12:43:52 +02:00
# Autopay has no limit on max licenses
2021-02-12 08:20:45 +01:00
check_success ( False , MAX_INVOICED_LICENSES + 1 , { " license_management " : " manual " } )
2018-12-22 05:29:25 +01:00
# Invoice
check_success ( True , self . seat_count + MIN_INVOICED_LICENSES )
2020-05-08 12:43:52 +02:00
# Invoice
check_success ( True , MAX_INVOICED_LICENSES )
2018-09-08 00:49:54 +02:00
2020-12-23 21:45:16 +01:00
def test_upgrade_with_uncaught_exception ( self ) - > None :
2021-02-12 08:20:45 +01:00
hamlet = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( hamlet )
2021-02-12 08:19:30 +01:00
with patch (
" corporate.views.process_initial_upgrade " , side_effect = Exception
2021-02-12 08:20:45 +01:00
) , self . assertLogs ( " corporate.stripe " , " WARNING " ) as m :
2018-11-29 03:15:27 +01:00
response = self . upgrade ( talk_to_stripe = False )
2021-02-12 08:20:45 +01:00
self . assertIn ( " ERROR:corporate.stripe:Uncaught exception in billing " , m . output [ 0 ] )
2020-12-23 21:45:16 +01:00
self . assertIn ( m . records [ 0 ] . stack_info , m . output [ 0 ] )
2021-02-12 08:19:30 +01:00
self . assert_json_error_contains (
response , " Something went wrong. Please contact desdemona+admin@zulip.com. "
)
self . assertEqual (
2021-02-12 08:20:45 +01:00
orjson . loads ( response . content ) [ " error_description " ] , " uncaught exception during upgrade "
2021-02-12 08:19:30 +01:00
)
2018-10-22 12:41:14 +02:00
2020-06-09 12:24:32 +02:00
def test_request_sponsorship ( self ) - > None :
user = self . example_user ( " hamlet " )
self . assertIsNone ( get_customer_by_realm ( user . realm ) )
self . login_user ( user )
data = {
2021-04-09 12:43:44 +02:00
" organization-type " : " Open-source " ,
" website " : " https://infinispan.org/ " ,
" description " : " Infinispan is a distributed in-memory key/value data store with optional schema. " ,
2020-06-09 12:24:32 +02:00
}
response = self . client_post ( " /json/billing/sponsorship " , data )
self . assert_json_success ( response )
customer = get_customer_by_realm ( user . realm )
2021-02-12 08:19:30 +01:00
assert customer is not None
2020-06-09 12:24:32 +02:00
self . assertEqual ( customer . sponsorship_pending , True )
from django . core . mail import outbox
2021-02-12 08:19:30 +01:00
2021-05-17 05:41:32 +02:00
self . assert_length ( outbox , 1 )
2020-06-09 12:24:32 +02:00
for message in outbox :
2021-05-17 05:41:32 +02:00
self . assert_length ( message . to , 1 )
2020-06-09 12:24:32 +02:00
self . assertEqual ( message . to [ 0 ] , " desdemona+admin@zulip.com " )
self . assertEqual ( message . subject , " Sponsorship request (Open-source) for zulip " )
2021-02-12 08:20:45 +01:00
self . assertEqual ( message . reply_to , [ " hamlet@zulip.com " ] )
2021-01-26 04:20:36 +01:00
self . assertEqual ( self . email_envelope_from ( message ) , settings . NOREPLY_EMAIL_ADDRESS )
self . assertIn ( " Zulip sponsorship <noreply- " , self . email_display_from ( message ) )
2020-07-17 14:47:15 +02:00
self . assertIn ( " Requested by: King Hamlet (Member) " , message . body )
2021-02-12 08:19:30 +01:00
self . assertIn (
" Support URL: http://zulip.testserver/activity/support?q=zulip " , message . body
)
2020-06-09 12:24:32 +02:00
self . assertIn ( " Website: https://infinispan.org " , message . body )
self . assertIn ( " Organization type: Open-source " , message . body )
self . assertIn ( " Description: \n Infinispan is a distributed in-memory " , message . body )
response = self . client_get ( " /upgrade/ " )
self . assertEqual ( response . status_code , 302 )
self . assertEqual ( response . url , " /billing/ " )
response = self . client_get ( " /billing/ " )
2021-02-12 08:19:30 +01:00
self . assert_in_success_response (
[ " Your organization has requested sponsored or discounted hosting. " ] , response
)
2020-06-09 12:24:32 +02:00
self . login_user ( self . example_user ( " othello " ) )
response = self . client_get ( " /billing/ " )
2021-02-12 08:19:30 +01:00
self . assert_in_success_response (
[ " You must be an organization owner or a billing administrator to view this page. " ] ,
response ,
)
2020-06-09 12:24:32 +02:00
2020-08-21 14:45:43 +02:00
user . realm . plan_type = Realm . STANDARD_FREE
user . realm . save ( )
self . login_user ( self . example_user ( " hamlet " ) )
response = self . client_get ( " /billing/ " )
2021-02-12 08:19:30 +01:00
self . assert_in_success_response (
[ " Your organization is fully sponsored and is on the <b>Zulip Standard</b> " ] , response
)
2020-08-21 14:45:43 +02:00
2018-11-28 10:49:16 +01:00
def test_redirect_for_billing_home ( self ) - > None :
2018-08-22 07:49:48 +02:00
user = self . example_user ( " iago " )
2020-03-06 18:40:46 +01:00
self . login_user ( user )
2018-03-31 04:13:44 +02:00
response = self . client_get ( " /billing/ " )
self . assertEqual ( response . status_code , 302 )
2021-02-12 08:20:45 +01:00
self . assertEqual ( " /upgrade/ " , response . url )
2018-03-31 04:13:44 +02:00
2020-08-21 14:45:43 +02:00
user . realm . plan_type = Realm . STANDARD_FREE
user . realm . save ( )
response = self . client_get ( " /billing/ " )
self . assertEqual ( response . status_code , 200 )
user . realm . plan_type = Realm . LIMITED
user . realm . save ( )
2021-02-12 08:20:45 +01:00
Customer . objects . create ( realm = user . realm , stripe_customer_id = " cus_123 " )
2018-11-05 22:37:22 +01:00
response = self . client_get ( " /billing/ " )
self . assertEqual ( response . status_code , 302 )
2021-02-12 08:20:45 +01:00
self . assertEqual ( " /upgrade/ " , response . url )
2018-11-05 22:37:22 +01:00
2020-05-22 15:42:46 +02:00
def test_redirect_for_upgrade_page ( self ) - > None :
user = self . example_user ( " iago " )
self . login_user ( user )
2020-08-21 14:45:43 +02:00
2020-05-22 15:42:46 +02:00
response = self . client_get ( " /upgrade/ " )
self . assertEqual ( response . status_code , 200 )
2020-08-21 14:45:43 +02:00
user . realm . plan_type = Realm . STANDARD_FREE
user . realm . save ( )
response = self . client_get ( " /upgrade/ " )
self . assertEqual ( response . status_code , 302 )
self . assertEqual ( response . url , " /billing/ " )
user . realm . plan_type = Realm . LIMITED
user . realm . save ( )
2021-02-12 08:20:45 +01:00
customer = Customer . objects . create ( realm = user . realm , stripe_customer_id = " cus_123 " )
2020-05-22 15:42:46 +02:00
response = self . client_get ( " /upgrade/ " )
self . assertEqual ( response . status_code , 200 )
2021-02-12 08:19:30 +01:00
CustomerPlan . objects . create (
customer = customer ,
billing_cycle_anchor = timezone_now ( ) ,
billing_schedule = CustomerPlan . ANNUAL ,
tier = CustomerPlan . STANDARD ,
)
2020-05-22 15:42:46 +02:00
response = self . client_get ( " /upgrade/ " )
self . assertEqual ( response . status_code , 302 )
self . assertEqual ( response . url , " /billing/ " )
with self . settings ( FREE_TRIAL_DAYS = 30 ) :
response = self . client_get ( " /upgrade/ " )
self . assertEqual ( response . status_code , 302 )
self . assertEqual ( response . url , " /billing/ " )
2020-09-13 00:11:30 +02:00
response = self . client_get ( " /upgrade/ " , { " onboarding " : " true " } )
2020-05-22 15:42:46 +02:00
self . assertEqual ( response . status_code , 302 )
self . assertEqual ( response . url , " /billing/?onboarding=true " )
2019-10-07 19:21:29 +02:00
def test_get_latest_seat_count ( self ) - > None :
2018-07-25 16:37:07 +02:00
realm = get_realm ( " zulip " )
2019-10-07 19:21:29 +02:00
initial_count = get_latest_seat_count ( realm )
2021-02-12 08:19:30 +01:00
user1 = UserProfile . objects . create (
2021-02-12 08:20:45 +01:00
realm = realm , email = " user1@zulip.com " , delivery_email = " user1@zulip.com "
2021-02-12 08:19:30 +01:00
)
user2 = UserProfile . objects . create (
2021-02-12 08:20:45 +01:00
realm = realm , email = " user2@zulip.com " , delivery_email = " user2@zulip.com "
2021-02-12 08:19:30 +01:00
)
2019-10-07 19:21:29 +02:00
self . assertEqual ( get_latest_seat_count ( realm ) , initial_count + 2 )
2018-03-31 04:13:44 +02:00
# Test that bots aren't counted
user1 . is_bot = True
2021-02-12 08:20:45 +01:00
user1 . save ( update_fields = [ " is_bot " ] )
2019-10-07 19:21:29 +02:00
self . assertEqual ( get_latest_seat_count ( realm ) , initial_count + 1 )
2018-03-31 04:13:44 +02:00
# Test that inactive users aren't counted
2021-03-27 06:02:12 +01:00
do_deactivate_user ( user2 , acting_user = None )
2019-10-07 19:21:29 +02:00
self . assertEqual ( get_latest_seat_count ( realm ) , initial_count )
2018-06-28 00:48:51 +02:00
2019-01-30 19:04:32 +01:00
# Test guests
# Adding a guest to a realm with a lot of members shouldn't change anything
2021-02-12 08:19:30 +01:00
UserProfile . objects . create (
realm = realm ,
2021-02-12 08:20:45 +01:00
email = " user3@zulip.com " ,
delivery_email = " user3@zulip.com " ,
2021-02-12 08:19:30 +01:00
role = UserProfile . ROLE_GUEST ,
)
2019-10-07 19:21:29 +02:00
self . assertEqual ( get_latest_seat_count ( realm ) , initial_count )
2019-01-30 19:04:32 +01:00
# Test 1 member and 5 guests
2021-03-08 13:22:43 +01:00
realm = do_create_realm ( string_id = " second " , name = " second " )
2021-02-12 08:19:30 +01:00
UserProfile . objects . create (
2021-02-12 08:20:45 +01:00
realm = realm , email = " member@second.com " , delivery_email = " member@second.com "
2021-02-12 08:19:30 +01:00
)
2019-01-30 19:04:32 +01:00
for i in range ( 5 ) :
2021-02-12 08:19:30 +01:00
UserProfile . objects . create (
realm = realm ,
2021-02-12 08:20:45 +01:00
email = f " guest { i } @second.com " ,
delivery_email = f " guest { i } @second.com " ,
2021-02-12 08:19:30 +01:00
role = UserProfile . ROLE_GUEST ,
)
2019-10-07 19:21:29 +02:00
self . assertEqual ( get_latest_seat_count ( realm ) , 1 )
2019-01-30 19:04:32 +01:00
# Test 1 member and 6 guests
2021-02-12 08:19:30 +01:00
UserProfile . objects . create (
realm = realm ,
2021-02-12 08:20:45 +01:00
email = " guest5@second.com " ,
delivery_email = " guest5@second.com " ,
2021-02-12 08:19:30 +01:00
role = UserProfile . ROLE_GUEST ,
)
2019-10-07 19:21:29 +02:00
self . assertEqual ( get_latest_seat_count ( realm ) , 2 )
2019-01-30 19:04:32 +01:00
2018-07-13 17:34:39 +02:00
def test_sign_string ( self ) - > None :
string = " abc "
signed_string , salt = sign_string ( string )
self . assertEqual ( string , unsign_string ( signed_string , salt ) )
with self . assertRaises ( signing . BadSignature ) :
unsign_string ( signed_string , " randomsalt " )
2018-09-08 00:49:54 +02:00
# This tests both the payment method string, and also is a very basic
# test that the various upgrade paths involving non-standard payment
# histories don't throw errors
2018-12-03 19:23:13 +01:00
@mock_stripe ( )
2018-12-02 19:05:34 +01:00
def test_payment_method_string ( self , * mocks : Mock ) - > None :
2018-12-15 09:33:25 +01:00
pass
2020-10-13 23:50:18 +02:00
# If you sign up with a card, we should show your card as the payment method
2018-09-08 00:49:54 +02:00
# Already tested in test_initial_upgrade
# If you pay by invoice, your payment method should be
# "Billed by invoice", even if you have a card on file
2018-12-15 09:33:25 +01:00
# user = self.example_user("hamlet")
2019-01-29 06:34:31 +01:00
# do_create_stripe_customer(user, stripe_create_token().id)
2020-03-06 18:40:46 +01:00
# self.login_user(user)
2018-12-15 09:33:25 +01:00
# self.upgrade(invoice=True)
# stripe_customer = stripe_get_customer(Customer.objects.get(realm=user.realm).stripe_customer_id)
# self.assertEqual('Billed by invoice', payment_method_string(stripe_customer))
2018-09-08 00:49:54 +02:00
2020-10-13 23:50:18 +02:00
# If you sign up with a card and then downgrade, we still have your
2018-09-08 00:49:54 +02:00
# card on file, and should show it
2018-12-12 07:47:53 +01:00
# TODO
2018-09-08 00:49:54 +02:00
2018-12-03 19:23:13 +01:00
@mock_stripe ( )
2018-12-02 19:05:34 +01:00
def test_attach_discount_to_realm ( self , * mocks : Mock ) - > None :
2018-11-23 12:37:01 +01:00
# Attach discount before Stripe customer exists
2021-02-12 08:20:45 +01:00
user = self . example_user ( " hamlet " )
2020-12-04 11:08:10 +01:00
attach_discount_to_realm ( user . realm , Decimal ( 85 ) , acting_user = user )
realm_audit_log = RealmAuditLog . objects . filter (
event_type = RealmAuditLog . REALM_DISCOUNT_CHANGED
) . last ( )
expected_extra_data = str ( { " old_discount " : None , " new_discount " : Decimal ( " 85 " ) } )
self . assertEqual ( realm_audit_log . extra_data , expected_extra_data )
2020-03-06 18:40:46 +01:00
self . login_user ( user )
2018-11-30 02:27:01 +01:00
# Check that the discount appears in page_params
2021-02-12 08:20:45 +01:00
self . assert_in_success_response ( [ " 85 " ] , self . client_get ( " /upgrade/ " ) )
2018-11-23 12:37:01 +01:00
# Check that the customer was charged the discounted amount
2019-01-29 06:34:31 +01:00
self . upgrade ( )
2020-12-04 15:14:59 +01:00
customer = Customer . objects . first ( )
[ charge ] = stripe . Charge . list ( customer = customer . stripe_customer_id )
2020-09-02 07:55:39 +02:00
self . assertEqual ( 1200 * self . seat_count , charge . amount )
2020-12-04 15:14:59 +01:00
[ invoice ] = stripe . Invoice . list ( customer = customer . stripe_customer_id )
2021-02-12 08:19:30 +01:00
self . assertEqual (
[ 1200 * self . seat_count , - 1200 * self . seat_count ] ,
[ item . amount for item in invoice . lines ] ,
)
2019-01-29 06:34:31 +01:00
# Check CustomerPlan reflects the discount
plan = CustomerPlan . objects . get ( price_per_license = 1200 , discount = Decimal ( 85 ) )
2018-11-23 12:37:01 +01:00
# Attach discount to existing Stripe customer
2019-01-29 06:34:31 +01:00
plan . status = CustomerPlan . ENDED
2021-02-12 08:20:45 +01:00
plan . save ( update_fields = [ " status " ] )
2020-12-04 11:08:10 +01:00
attach_discount_to_realm ( user . realm , Decimal ( 25 ) , acting_user = user )
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2021-02-12 08:19:30 +01:00
process_initial_upgrade (
user , self . seat_count , True , CustomerPlan . ANNUAL , stripe_create_token ( ) . id
)
2020-12-04 15:14:59 +01:00
[ charge , _ ] = stripe . Charge . list ( customer = customer . stripe_customer_id )
self . assertEqual ( 6000 * self . seat_count , charge . amount )
[ invoice , _ ] = stripe . Invoice . list ( customer = customer . stripe_customer_id )
2021-02-12 08:19:30 +01:00
self . assertEqual (
[ 6000 * self . seat_count , - 6000 * self . seat_count ] ,
[ item . amount for item in invoice . lines ] ,
)
2019-01-29 06:34:31 +01:00
plan = CustomerPlan . objects . get ( price_per_license = 6000 , discount = Decimal ( 25 ) )
2018-08-23 07:45:19 +02:00
2020-12-04 11:08:10 +01:00
attach_discount_to_realm ( user . realm , Decimal ( 50 ) , acting_user = user )
2020-12-04 15:14:59 +01:00
plan . refresh_from_db ( )
self . assertEqual ( plan . price_per_license , 4000 )
self . assertEqual ( plan . discount , 50 )
customer . refresh_from_db ( )
self . assertEqual ( customer . default_discount , 50 )
invoice_plans_as_needed ( self . next_year + timedelta ( days = 10 ) )
[ invoice , _ , _ ] = stripe . Invoice . list ( customer = customer . stripe_customer_id )
2021-02-12 08:19:30 +01:00
self . assertEqual ( [ 4000 * self . seat_count ] , [ item . amount for item in invoice . lines ] )
2020-12-04 11:08:10 +01:00
realm_audit_log = RealmAuditLog . objects . filter (
event_type = RealmAuditLog . REALM_DISCOUNT_CHANGED
) . last ( )
expected_extra_data = str (
{ " old_discount " : Decimal ( " 25.0000 " ) , " new_discount " : Decimal ( " 50 " ) }
)
self . assertEqual ( realm_audit_log . extra_data , expected_extra_data )
self . assertEqual ( realm_audit_log . acting_user , user )
2020-12-04 15:14:59 +01:00
2020-12-04 11:16:33 +01:00
def test_approve_sponsorship ( self ) - > None :
user = self . example_user ( " hamlet " )
approve_sponsorship ( user . realm , acting_user = user )
realm = get_realm ( " zulip " )
self . assertEqual ( realm . plan_type , Realm . STANDARD_FREE )
expected_message = " Your organization ' s request for sponsored hosting has been approved! :tada:. \n You have been upgraded to Zulip Cloud Standard, free of charge. "
sender = UserProfile . objects . filter ( email = settings . NOTIFICATION_BOT ) . first ( )
recipient_id = UserProfile . objects . filter ( email = " desdemona@zulip.com " ) . first ( ) . recipient_id
message = Message . objects . filter ( sender = sender . id ) . first ( )
self . assertEqual ( message . content , expected_message )
self . assertEqual ( message . recipient . type , Recipient . PERSONAL )
self . assertEqual ( message . recipient_id , recipient_id )
2020-12-04 12:14:51 +01:00
def test_update_sponsorship_status ( self ) - > None :
lear = get_realm ( " lear " )
iago = self . example_user ( " iago " )
update_sponsorship_status ( lear , True , acting_user = iago )
customer = get_customer_by_realm ( realm = lear )
assert customer is not None
self . assertTrue ( customer . sponsorship_pending )
realm_audit_log = RealmAuditLog . objects . filter (
event_type = RealmAuditLog . REALM_SPONSORSHIP_PENDING_STATUS_CHANGED
) . last ( )
expected_extra_data = { " sponsorship_pending " : True }
self . assertEqual ( realm_audit_log . extra_data , str ( expected_extra_data ) )
self . assertEqual ( realm_audit_log . acting_user , iago )
2019-03-06 13:01:56 +01:00
def test_get_discount_for_realm ( self ) - > None :
2021-02-12 08:20:45 +01:00
user = self . example_user ( " hamlet " )
2019-03-06 13:01:56 +01:00
self . assertEqual ( get_discount_for_realm ( user . realm ) , None )
2020-12-04 11:08:10 +01:00
attach_discount_to_realm ( user . realm , Decimal ( 85 ) , acting_user = None )
2019-03-06 13:01:56 +01:00
self . assertEqual ( get_discount_for_realm ( user . realm ) , 85 )
2018-12-03 19:23:13 +01:00
@mock_stripe ( )
2018-12-02 19:05:34 +01:00
def test_replace_payment_source ( self , * mocks : Mock ) - > None :
2018-11-28 09:07:21 +01:00
user = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( user )
2018-11-29 03:15:27 +01:00
self . upgrade ( )
2019-04-04 10:02:49 +02:00
# Create an open invoice
stripe_customer_id = Customer . objects . first ( ) . stripe_customer_id
2021-02-12 08:20:45 +01:00
stripe . InvoiceItem . create ( amount = 5000 , currency = " usd " , customer = stripe_customer_id )
2019-04-04 10:02:49 +02:00
stripe_invoice = stripe . Invoice . create ( customer = stripe_customer_id )
stripe . Invoice . finalize_invoice ( stripe_invoice )
RealmAuditLog . objects . filter ( event_type = RealmAuditLog . STRIPE_CARD_CHANGED ) . delete ( )
2018-11-28 09:07:21 +01:00
2019-04-04 10:02:49 +02:00
# Replace with an invalid card
2021-02-12 08:20:45 +01:00
stripe_token = stripe_create_token ( card_number = " 4000000000009987 " ) . id
2021-02-12 08:19:30 +01:00
with patch ( " stripe.Invoice.list " ) as mock_invoice_list , self . assertLogs (
2021-02-12 08:20:45 +01:00
" corporate.stripe " , " INFO "
2021-02-12 08:19:30 +01:00
) as m :
response = self . client_post (
" /json/billing/sources/change " ,
2021-02-12 08:20:45 +01:00
{ " stripe_token " : orjson . dumps ( stripe_token ) . decode ( ) } ,
2021-02-12 08:19:30 +01:00
)
self . assertEqual (
2021-02-12 08:20:45 +01:00
m . output , [ " INFO:corporate.stripe:Stripe card error: 402 card_error card_declined " ]
2021-02-12 08:19:30 +01:00
)
2019-04-04 10:02:49 +02:00
mock_invoice_list . assert_not_called ( )
2021-02-12 08:20:45 +01:00
self . assertEqual ( orjson . loads ( response . content ) [ " error_description " ] , " card error " )
self . assert_json_error_contains ( response , " Your card was declined " )
2019-04-04 10:02:49 +02:00
for stripe_source in stripe_get_customer ( stripe_customer_id ) . sources :
2020-06-23 00:31:30 +02:00
assert isinstance ( stripe_source , stripe . Card )
2021-02-12 08:20:45 +01:00
self . assertEqual ( stripe_source . last4 , " 4242 " )
2021-02-12 08:19:30 +01:00
self . assertFalse (
RealmAuditLog . objects . filter ( event_type = RealmAuditLog . STRIPE_CARD_CHANGED ) . exists ( )
)
2019-04-04 10:02:49 +02:00
# Replace with a card that's valid, but charging the card fails
2021-02-12 08:20:45 +01:00
stripe_token = stripe_create_token ( card_number = " 4000000000000341 " ) . id
with self . assertLogs ( " corporate.stripe " , " INFO " ) as m :
2021-02-12 08:19:30 +01:00
response = self . client_post (
" /json/billing/sources/change " ,
2021-02-12 08:20:45 +01:00
{ " stripe_token " : orjson . dumps ( stripe_token ) . decode ( ) } ,
2021-02-12 08:19:30 +01:00
)
self . assertEqual (
m . output ,
2021-02-12 08:20:45 +01:00
[ " INFO:corporate.stripe:Stripe card error: 402 card_error card_declined None " ] ,
2021-02-12 08:19:30 +01:00
)
2021-02-12 08:20:45 +01:00
self . assertEqual ( orjson . loads ( response . content ) [ " error_description " ] , " card error " )
self . assert_json_error_contains ( response , " Your card was declined " )
2019-04-04 10:02:49 +02:00
for stripe_source in stripe_get_customer ( stripe_customer_id ) . sources :
2020-06-23 00:31:30 +02:00
assert isinstance ( stripe_source , stripe . Card )
2021-02-12 08:20:45 +01:00
self . assertEqual ( stripe_source . last4 , " 0341 " )
2021-02-12 08:19:30 +01:00
self . assertEqual (
2021-02-12 08:20:45 +01:00
len ( list ( stripe . Invoice . list ( customer = stripe_customer_id , status = " open " ) ) ) , 1
2021-02-12 08:19:30 +01:00
)
self . assertEqual (
1 , RealmAuditLog . objects . filter ( event_type = RealmAuditLog . STRIPE_CARD_CHANGED ) . count ( )
)
2019-04-04 10:02:49 +02:00
# Replace with a valid card
2021-02-12 08:20:45 +01:00
stripe_token = stripe_create_token ( card_number = " 5555555555554444 " ) . id
2021-02-12 08:19:30 +01:00
response = self . client_post (
2021-02-12 08:20:45 +01:00
" /json/billing/sources/change " , { " stripe_token " : orjson . dumps ( stripe_token ) . decode ( ) }
2021-02-12 08:19:30 +01:00
)
2019-04-04 10:02:49 +02:00
self . assert_json_success ( response )
2018-11-28 09:07:21 +01:00
number_of_sources = 0
2019-04-04 10:02:49 +02:00
for stripe_source in stripe_get_customer ( stripe_customer_id ) . sources :
2020-06-23 00:31:30 +02:00
assert isinstance ( stripe_source , stripe . Card )
2021-02-12 08:20:45 +01:00
self . assertEqual ( stripe_source . last4 , " 4444 " )
2018-11-28 09:07:21 +01:00
number_of_sources + = 1
2019-04-04 10:02:49 +02:00
# Verify that we replaced the previous card, rather than adding a new one
2018-11-28 09:07:21 +01:00
self . assertEqual ( number_of_sources , 1 )
2019-04-04 10:02:49 +02:00
# Ideally we'd also test that we don't pay invoices with billing=='send_invoice'
for stripe_invoice in stripe . Invoice . list ( customer = stripe_customer_id ) :
2021-02-12 08:20:45 +01:00
self . assertEqual ( stripe_invoice . status , " paid " )
2021-02-12 08:19:30 +01:00
self . assertEqual (
2 , RealmAuditLog . objects . filter ( event_type = RealmAuditLog . STRIPE_CARD_CHANGED ) . count ( )
)
2018-10-22 14:21:48 +02:00
2020-12-23 21:45:16 +01:00
def test_downgrade ( self ) - > None :
2019-04-08 05:16:35 +02:00
user = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( user )
2019-04-08 05:16:35 +02:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2020-12-30 18:57:35 +01:00
plan = get_current_plan_by_realm ( user . realm )
assert plan is not None
self . assertEqual ( plan . licenses ( ) , self . seat_count )
self . assertEqual ( plan . licenses_at_next_renewal ( ) , self . seat_count )
2021-02-12 08:20:45 +01:00
with self . assertLogs ( " corporate.stripe " , " INFO " ) as m :
2020-12-17 16:35:33 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
response = self . client_patch (
" /json/billing/plan " , { " status " : CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE }
)
stripe_customer_id = Customer . objects . get ( realm = user . realm ) . id
new_plan = get_current_plan_by_realm ( user . realm )
assert new_plan is not None
expected_log = f " INFO:corporate.stripe:Change plan status: Customer.id: { stripe_customer_id } , CustomerPlan.id: { new_plan . id } , status: { CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE } "
self . assertEqual ( m . output [ 0 ] , expected_log )
self . assert_json_success ( response )
2020-12-30 18:57:35 +01:00
plan . refresh_from_db ( )
self . assertEqual ( plan . licenses ( ) , self . seat_count )
self . assertEqual ( plan . licenses_at_next_renewal ( ) , None )
2019-04-08 05:16:35 +02:00
2020-12-30 15:53:43 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
mock_customer = Mock ( email = user . delivery_email , default_source = None )
with patch ( " corporate.views.stripe_get_customer " , return_value = mock_customer ) :
response = self . client_get ( " /billing/ " )
2021-01-25 19:13:52 +01:00
self . assert_in_success_response (
[
" Your plan will be downgraded to <strong>Zulip Limited</strong> on "
" <strong>January 2, 2013</strong> " ,
" You plan is scheduled for downgrade on <strong>January 2, 2013</strong> " ,
" Cancel downgrade " ,
] ,
response ,
)
2020-12-30 15:53:43 +01:00
2019-04-08 05:16:35 +02:00
# Verify that we still write LicenseLedger rows during the remaining
# part of the cycle
2019-10-07 19:21:29 +02:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 20 ) :
2019-04-08 05:16:35 +02:00
update_license_ledger_if_needed ( user . realm , self . now )
2021-02-12 08:19:30 +01:00
self . assertEqual (
2021-02-12 08:20:45 +01:00
LicenseLedger . objects . order_by ( " -id " )
. values_list ( " licenses " , " licenses_at_next_renewal " )
2021-02-12 08:19:30 +01:00
. first ( ) ,
( 20 , 20 ) ,
)
2019-04-08 05:16:35 +02:00
# Verify that we invoice them for the additional users
from stripe import Invoice
2021-02-12 08:19:30 +01:00
2020-04-22 04:13:37 +02:00
Invoice . create = lambda * * args : None # type: ignore[assignment] # cleaner than mocking
Invoice . finalize_invoice = lambda * args : None # type: ignore[assignment] # cleaner than mocking
2019-04-08 05:16:35 +02:00
with patch ( " stripe.InvoiceItem.create " ) as mocked :
invoice_plans_as_needed ( self . next_month )
mocked . assert_called_once ( )
mocked . reset_mock ( )
# Check that we downgrade properly if the cycle is over
2019-10-07 19:21:29 +02:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 30 ) :
2019-04-08 05:16:35 +02:00
update_license_ledger_if_needed ( user . realm , self . next_year )
2021-02-12 08:20:45 +01:00
self . assertEqual ( get_realm ( " zulip " ) . plan_type , Realm . LIMITED )
2019-04-08 05:16:35 +02:00
self . assertEqual ( CustomerPlan . objects . first ( ) . status , CustomerPlan . ENDED )
2021-02-12 08:19:30 +01:00
self . assertEqual (
2021-02-12 08:20:45 +01:00
LicenseLedger . objects . order_by ( " -id " )
. values_list ( " licenses " , " licenses_at_next_renewal " )
2021-02-12 08:19:30 +01:00
. first ( ) ,
( 20 , 20 ) ,
)
2019-04-08 05:16:35 +02:00
# Verify that we don't write LicenseLedger rows once we've downgraded
2019-10-07 19:21:29 +02:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 40 ) :
2019-04-08 05:16:35 +02:00
update_license_ledger_if_needed ( user . realm , self . next_year )
2021-02-12 08:19:30 +01:00
self . assertEqual (
2021-02-12 08:20:45 +01:00
LicenseLedger . objects . order_by ( " -id " )
. values_list ( " licenses " , " licenses_at_next_renewal " )
2021-02-12 08:19:30 +01:00
. first ( ) ,
( 20 , 20 ) ,
)
2019-04-08 05:16:35 +02:00
# Verify that we call invoice_plan once more after cycle end but
# don't invoice them for users added after the cycle end
self . assertIsNotNone ( CustomerPlan . objects . first ( ) . next_invoice_date )
with patch ( " stripe.InvoiceItem.create " ) as mocked :
invoice_plans_as_needed ( self . next_year + timedelta ( days = 32 ) )
mocked . assert_not_called ( )
mocked . reset_mock ( )
# Check that we updated next_invoice_date in invoice_plan
self . assertIsNone ( CustomerPlan . objects . first ( ) . next_invoice_date )
# Check that we don't call invoice_plan after that final call
2019-10-07 19:21:29 +02:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 50 ) :
2019-04-08 05:16:35 +02:00
update_license_ledger_if_needed ( user . realm , self . next_year + timedelta ( days = 80 ) )
with patch ( " corporate.lib.stripe.invoice_plan " ) as mocked :
invoice_plans_as_needed ( self . next_year + timedelta ( days = 400 ) )
mocked . assert_not_called ( )
2020-06-15 20:09:24 +02:00
@mock_stripe ( )
2021-02-12 08:19:30 +01:00
def test_switch_from_monthly_plan_to_annual_plan_for_automatic_license_management (
self , * mocks : Mock
) - > None :
2020-06-15 20:09:24 +02:00
user = self . example_user ( " hamlet " )
self . login_user ( user )
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
self . upgrade ( schedule = " monthly " )
2020-06-15 20:09:24 +02:00
monthly_plan = get_current_plan_by_realm ( user . realm )
2021-02-12 08:19:30 +01:00
assert monthly_plan is not None
2020-06-15 20:09:24 +02:00
self . assertEqual ( monthly_plan . automanage_licenses , True )
self . assertEqual ( monthly_plan . billing_schedule , CustomerPlan . MONTHLY )
2021-02-04 11:20:53 +01:00
stripe_customer_id = Customer . objects . get ( realm = user . realm ) . id
new_plan = get_current_plan_by_realm ( user . realm )
assert new_plan is not None
2021-02-12 08:20:45 +01:00
with self . assertLogs ( " corporate.stripe " , " INFO " ) as m :
2020-12-17 16:35:33 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
response = self . client_patch (
" /json/billing/plan " ,
{ " status " : CustomerPlan . SWITCH_TO_ANNUAL_AT_END_OF_CYCLE } ,
)
expected_log = f " INFO:corporate.stripe:Change plan status: Customer.id: { stripe_customer_id } , CustomerPlan.id: { new_plan . id } , status: { CustomerPlan . SWITCH_TO_ANNUAL_AT_END_OF_CYCLE } "
self . assertEqual ( m . output [ 0 ] , expected_log )
self . assert_json_success ( response )
2020-06-15 20:09:24 +02:00
monthly_plan . refresh_from_db ( )
self . assertEqual ( monthly_plan . status , CustomerPlan . SWITCH_TO_ANNUAL_AT_END_OF_CYCLE )
2021-02-12 08:20:45 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
2020-06-15 20:09:24 +02:00
response = self . client_get ( " /billing/ " )
2021-02-12 08:19:30 +01:00
self . assert_in_success_response (
[ " be switched from monthly to annual billing on <strong>February 2, 2012 " ] , response
)
2020-06-15 20:09:24 +02:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 20 ) :
update_license_ledger_if_needed ( user . realm , self . now )
self . assertEqual ( LicenseLedger . objects . filter ( plan = monthly_plan ) . count ( ) , 2 )
2021-02-12 08:19:30 +01:00
self . assertEqual (
2021-02-12 08:20:45 +01:00
LicenseLedger . objects . order_by ( " -id " )
. values_list ( " licenses " , " licenses_at_next_renewal " )
2021-02-12 08:19:30 +01:00
. first ( ) ,
( 20 , 20 ) ,
)
2020-06-15 20:09:24 +02:00
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . next_month ) :
2020-06-15 20:09:24 +02:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 25 ) :
update_license_ledger_if_needed ( user . realm , self . next_month )
self . assertEqual ( LicenseLedger . objects . filter ( plan = monthly_plan ) . count ( ) , 2 )
customer = get_customer_by_realm ( user . realm )
2021-02-12 08:19:30 +01:00
assert customer is not None
2020-06-15 20:09:24 +02:00
self . assertEqual ( CustomerPlan . objects . filter ( customer = customer ) . count ( ) , 2 )
monthly_plan . refresh_from_db ( )
self . assertEqual ( monthly_plan . status , CustomerPlan . ENDED )
self . assertEqual ( monthly_plan . next_invoice_date , self . next_month )
annual_plan = get_current_plan_by_realm ( user . realm )
2021-02-12 08:19:30 +01:00
assert annual_plan is not None
2020-06-15 20:09:24 +02:00
self . assertEqual ( annual_plan . status , CustomerPlan . ACTIVE )
self . assertEqual ( annual_plan . billing_schedule , CustomerPlan . ANNUAL )
self . assertEqual ( annual_plan . invoicing_status , CustomerPlan . INITIAL_INVOICE_TO_BE_SENT )
self . assertEqual ( annual_plan . billing_cycle_anchor , self . next_month )
self . assertEqual ( annual_plan . next_invoice_date , self . next_month )
self . assertEqual ( annual_plan . invoiced_through , None )
2021-02-12 08:20:45 +01:00
annual_ledger_entries = LicenseLedger . objects . filter ( plan = annual_plan ) . order_by ( " id " )
2021-05-17 05:41:32 +02:00
self . assert_length ( annual_ledger_entries , 2 )
2020-06-15 20:09:24 +02:00
self . assertEqual ( annual_ledger_entries [ 0 ] . is_renewal , True )
2021-02-12 08:19:30 +01:00
self . assertEqual (
2021-02-12 08:20:45 +01:00
annual_ledger_entries . values_list ( " licenses " , " licenses_at_next_renewal " ) [ 0 ] , ( 20 , 20 )
2021-02-12 08:19:30 +01:00
)
2020-06-15 20:09:24 +02:00
self . assertEqual ( annual_ledger_entries [ 1 ] . is_renewal , False )
2021-02-12 08:19:30 +01:00
self . assertEqual (
2021-02-12 08:20:45 +01:00
annual_ledger_entries . values_list ( " licenses " , " licenses_at_next_renewal " ) [ 1 ] , ( 25 , 25 )
2021-02-12 08:19:30 +01:00
)
audit_log = RealmAuditLog . objects . get (
event_type = RealmAuditLog . CUSTOMER_SWITCHED_FROM_MONTHLY_TO_ANNUAL_PLAN
)
2020-06-18 17:57:27 +02:00
self . assertEqual ( audit_log . realm , user . realm )
2020-08-07 01:09:47 +02:00
self . assertEqual ( orjson . loads ( audit_log . extra_data ) [ " monthly_plan_id " ] , monthly_plan . id )
self . assertEqual ( orjson . loads ( audit_log . extra_data ) [ " annual_plan_id " ] , annual_plan . id )
2020-06-15 20:09:24 +02:00
invoice_plans_as_needed ( self . next_month )
2021-02-12 08:20:45 +01:00
annual_ledger_entries = LicenseLedger . objects . filter ( plan = annual_plan ) . order_by ( " id " )
2021-05-17 05:41:32 +02:00
self . assert_length ( annual_ledger_entries , 2 )
2020-06-15 20:09:24 +02:00
annual_plan . refresh_from_db ( )
self . assertEqual ( annual_plan . invoicing_status , CustomerPlan . DONE )
self . assertEqual ( annual_plan . invoiced_through , annual_ledger_entries [ 1 ] )
self . assertEqual ( annual_plan . billing_cycle_anchor , self . next_month )
self . assertEqual ( annual_plan . next_invoice_date , add_months ( self . next_month , 1 ) )
monthly_plan . refresh_from_db ( )
self . assertEqual ( monthly_plan . next_invoice_date , None )
2021-06-18 21:10:45 +02:00
assert customer . stripe_customer_id
2020-09-02 07:55:39 +02:00
[ invoice0 , invoice1 , invoice2 ] = stripe . Invoice . list ( customer = customer . stripe_customer_id )
2020-06-15 20:09:24 +02:00
2020-09-02 07:55:39 +02:00
[ invoice_item0 , invoice_item1 ] = invoice0 . get ( " lines " )
2020-06-15 20:09:24 +02:00
annual_plan_invoice_item_params = {
" amount " : 5 * 80 * 100 ,
" description " : " Additional license (Feb 2, 2012 - Feb 2, 2013) " ,
2021-02-12 08:19:30 +01:00
" plan " : None ,
" quantity " : 5 ,
" subscription " : None ,
" discountable " : False ,
2020-06-15 20:09:24 +02:00
" period " : {
" start " : datetime_to_timestamp ( self . next_month ) ,
2021-02-12 08:19:30 +01:00
" end " : datetime_to_timestamp ( add_months ( self . next_month , 12 ) ) ,
2020-06-15 20:09:24 +02:00
} ,
}
for key , value in annual_plan_invoice_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( invoice_item0 [ key ] , value )
2020-06-15 20:09:24 +02:00
annual_plan_invoice_item_params = {
2021-02-12 08:19:30 +01:00
" amount " : 20 * 80 * 100 ,
" description " : " Zulip Standard - renewal " ,
" plan " : None ,
" quantity " : 20 ,
" subscription " : None ,
" discountable " : False ,
2020-06-15 20:09:24 +02:00
" period " : {
" start " : datetime_to_timestamp ( self . next_month ) ,
2021-02-12 08:19:30 +01:00
" end " : datetime_to_timestamp ( add_months ( self . next_month , 12 ) ) ,
2020-06-15 20:09:24 +02:00
} ,
}
for key , value in annual_plan_invoice_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( invoice_item1 [ key ] , value )
2020-06-15 20:09:24 +02:00
2020-09-02 07:55:39 +02:00
[ monthly_plan_invoice_item ] = invoice1 . get ( " lines " )
2020-06-15 20:09:24 +02:00
monthly_plan_invoice_item_params = {
" amount " : 14 * 8 * 100 ,
" description " : " Additional license (Jan 2, 2012 - Feb 2, 2012) " ,
2021-02-12 08:19:30 +01:00
" plan " : None ,
" quantity " : 14 ,
" subscription " : None ,
" discountable " : False ,
2020-06-15 20:09:24 +02:00
" period " : {
" start " : datetime_to_timestamp ( self . now ) ,
2021-02-12 08:19:30 +01:00
" end " : datetime_to_timestamp ( self . next_month ) ,
2020-06-15 20:09:24 +02:00
} ,
}
for key , value in monthly_plan_invoice_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( monthly_plan_invoice_item [ key ] , value )
2020-06-15 20:09:24 +02:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 30 ) :
update_license_ledger_if_needed ( user . realm , add_months ( self . next_month , 1 ) )
invoice_plans_as_needed ( add_months ( self . next_month , 1 ) )
2021-02-12 08:19:30 +01:00
[ invoice0 , invoice1 , invoice2 , invoice3 ] = stripe . Invoice . list (
customer = customer . stripe_customer_id
)
2020-06-15 20:09:24 +02:00
2020-09-02 07:55:39 +02:00
[ monthly_plan_invoice_item ] = invoice0 . get ( " lines " )
2020-06-15 20:09:24 +02:00
monthly_plan_invoice_item_params = {
" amount " : 5 * 7366 ,
" description " : " Additional license (Mar 2, 2012 - Feb 2, 2013) " ,
2021-02-12 08:19:30 +01:00
" plan " : None ,
" quantity " : 5 ,
" subscription " : None ,
" discountable " : False ,
2020-06-15 20:09:24 +02:00
" period " : {
" start " : datetime_to_timestamp ( add_months ( self . next_month , 1 ) ) ,
2021-02-12 08:19:30 +01:00
" end " : datetime_to_timestamp ( add_months ( self . next_month , 12 ) ) ,
2020-06-15 20:09:24 +02:00
} ,
}
for key , value in monthly_plan_invoice_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( monthly_plan_invoice_item [ key ] , value )
2020-06-15 20:09:24 +02:00
invoice_plans_as_needed ( add_months ( self . now , 13 ) )
2021-02-12 08:19:30 +01:00
[ invoice0 , invoice1 , invoice2 , invoice3 , invoice4 ] = stripe . Invoice . list (
customer = customer . stripe_customer_id
)
2020-06-15 20:09:24 +02:00
2020-09-02 07:55:39 +02:00
[ invoice_item ] = invoice0 . get ( " lines " )
2020-06-15 20:09:24 +02:00
annual_plan_invoice_item_params = {
" amount " : 30 * 80 * 100 ,
" description " : " Zulip Standard - renewal " ,
2021-02-12 08:19:30 +01:00
" plan " : None ,
" quantity " : 30 ,
" subscription " : None ,
" discountable " : False ,
2020-06-15 20:09:24 +02:00
" period " : {
" start " : datetime_to_timestamp ( add_months ( self . next_month , 12 ) ) ,
2021-02-12 08:19:30 +01:00
" end " : datetime_to_timestamp ( add_months ( self . next_month , 24 ) ) ,
2020-06-15 20:09:24 +02:00
} ,
}
for key , value in annual_plan_invoice_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( invoice_item [ key ] , value )
2020-06-15 20:09:24 +02:00
@mock_stripe ( )
2021-02-12 08:19:30 +01:00
def test_switch_from_monthly_plan_to_annual_plan_for_manual_license_management (
self , * mocks : Mock
) - > None :
2020-06-15 20:09:24 +02:00
user = self . example_user ( " hamlet " )
num_licenses = 35
self . login_user ( user )
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
self . upgrade ( schedule = " monthly " , license_management = " manual " , licenses = num_licenses )
2020-06-15 20:09:24 +02:00
monthly_plan = get_current_plan_by_realm ( user . realm )
2021-02-12 08:19:30 +01:00
assert monthly_plan is not None
2020-06-15 20:09:24 +02:00
self . assertEqual ( monthly_plan . automanage_licenses , False )
self . assertEqual ( monthly_plan . billing_schedule , CustomerPlan . MONTHLY )
2021-02-04 11:20:53 +01:00
stripe_customer_id = Customer . objects . get ( realm = user . realm ) . id
new_plan = get_current_plan_by_realm ( user . realm )
assert new_plan is not None
2021-02-12 08:20:45 +01:00
with self . assertLogs ( " corporate.stripe " , " INFO " ) as m :
2020-12-17 16:35:33 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
response = self . client_patch (
" /json/billing/plan " ,
{ " status " : CustomerPlan . SWITCH_TO_ANNUAL_AT_END_OF_CYCLE } ,
)
self . assertEqual (
m . output [ 0 ] ,
f " INFO:corporate.stripe:Change plan status: Customer.id: { stripe_customer_id } , CustomerPlan.id: { new_plan . id } , status: { CustomerPlan . SWITCH_TO_ANNUAL_AT_END_OF_CYCLE } " ,
)
self . assert_json_success ( response )
2020-06-15 20:09:24 +02:00
monthly_plan . refresh_from_db ( )
self . assertEqual ( monthly_plan . status , CustomerPlan . SWITCH_TO_ANNUAL_AT_END_OF_CYCLE )
2021-02-12 08:20:45 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
2020-06-15 20:09:24 +02:00
response = self . client_get ( " /billing/ " )
2021-02-12 08:19:30 +01:00
self . assert_in_success_response (
[ " be switched from monthly to annual billing on <strong>February 2, 2012 " ] , response
)
2020-06-15 20:09:24 +02:00
2020-06-18 17:57:27 +02:00
invoice_plans_as_needed ( self . next_month )
2020-06-15 20:09:24 +02:00
self . assertEqual ( LicenseLedger . objects . filter ( plan = monthly_plan ) . count ( ) , 1 )
customer = get_customer_by_realm ( user . realm )
2021-02-12 08:19:30 +01:00
assert customer is not None
2020-06-15 20:09:24 +02:00
self . assertEqual ( CustomerPlan . objects . filter ( customer = customer ) . count ( ) , 2 )
monthly_plan . refresh_from_db ( )
self . assertEqual ( monthly_plan . status , CustomerPlan . ENDED )
self . assertEqual ( monthly_plan . next_invoice_date , None )
annual_plan = get_current_plan_by_realm ( user . realm )
2021-02-12 08:19:30 +01:00
assert annual_plan is not None
2020-06-15 20:09:24 +02:00
self . assertEqual ( annual_plan . status , CustomerPlan . ACTIVE )
self . assertEqual ( annual_plan . billing_schedule , CustomerPlan . ANNUAL )
self . assertEqual ( annual_plan . invoicing_status , CustomerPlan . INITIAL_INVOICE_TO_BE_SENT )
self . assertEqual ( annual_plan . billing_cycle_anchor , self . next_month )
self . assertEqual ( annual_plan . next_invoice_date , self . next_month )
2021-02-12 08:20:45 +01:00
annual_ledger_entries = LicenseLedger . objects . filter ( plan = annual_plan ) . order_by ( " id " )
2021-05-17 05:41:32 +02:00
self . assert_length ( annual_ledger_entries , 1 )
2020-06-15 20:09:24 +02:00
self . assertEqual ( annual_ledger_entries [ 0 ] . is_renewal , True )
2021-02-12 08:19:30 +01:00
self . assertEqual (
2021-02-12 08:20:45 +01:00
annual_ledger_entries . values_list ( " licenses " , " licenses_at_next_renewal " ) [ 0 ] ,
2021-02-12 08:19:30 +01:00
( num_licenses , num_licenses ) ,
)
2020-06-15 20:09:24 +02:00
self . assertEqual ( annual_plan . invoiced_through , None )
2020-06-18 17:57:27 +02:00
# First call of invoice_plans_as_needed creates the new plan. Second call
# calls invoice_plan on the newly created plan.
invoice_plans_as_needed ( self . next_month + timedelta ( days = 1 ) )
2020-06-15 20:09:24 +02:00
annual_plan . refresh_from_db ( )
self . assertEqual ( annual_plan . invoiced_through , annual_ledger_entries [ 0 ] )
self . assertEqual ( annual_plan . next_invoice_date , add_months ( self . next_month , 12 ) )
self . assertEqual ( annual_plan . invoicing_status , CustomerPlan . DONE )
2021-06-18 21:10:45 +02:00
assert customer . stripe_customer_id
2020-09-02 07:55:39 +02:00
[ invoice0 , invoice1 ] = stripe . Invoice . list ( customer = customer . stripe_customer_id )
2020-06-15 20:09:24 +02:00
2020-09-02 07:55:39 +02:00
[ invoice_item ] = invoice0 . get ( " lines " )
2020-06-15 20:09:24 +02:00
annual_plan_invoice_item_params = {
2021-02-12 08:19:30 +01:00
" amount " : num_licenses * 80 * 100 ,
" description " : " Zulip Standard - renewal " ,
" plan " : None ,
" quantity " : num_licenses ,
" subscription " : None ,
" discountable " : False ,
2020-06-15 20:09:24 +02:00
" period " : {
" start " : datetime_to_timestamp ( self . next_month ) ,
2021-02-12 08:19:30 +01:00
" end " : datetime_to_timestamp ( add_months ( self . next_month , 12 ) ) ,
2020-06-15 20:09:24 +02:00
} ,
}
for key , value in annual_plan_invoice_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( invoice_item [ key ] , value )
2020-06-15 20:09:24 +02:00
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.invoice_plan " ) as m :
2020-06-15 20:09:24 +02:00
invoice_plans_as_needed ( add_months ( self . now , 2 ) )
m . assert_not_called ( )
invoice_plans_as_needed ( add_months ( self . now , 13 ) )
2020-09-02 07:55:39 +02:00
[ invoice0 , invoice1 , invoice2 ] = stripe . Invoice . list ( customer = customer . stripe_customer_id )
2020-06-15 20:09:24 +02:00
2020-09-02 07:55:39 +02:00
[ invoice_item ] = invoice0 . get ( " lines " )
2020-06-15 20:09:24 +02:00
annual_plan_invoice_item_params = {
" amount " : num_licenses * 80 * 100 ,
" description " : " Zulip Standard - renewal " ,
2021-02-12 08:19:30 +01:00
" plan " : None ,
" quantity " : num_licenses ,
" subscription " : None ,
" discountable " : False ,
2020-06-15 20:09:24 +02:00
" period " : {
" start " : datetime_to_timestamp ( add_months ( self . next_month , 12 ) ) ,
2021-02-12 08:19:30 +01:00
" end " : datetime_to_timestamp ( add_months ( self . next_month , 24 ) ) ,
2020-06-15 20:09:24 +02:00
} ,
}
for key , value in annual_plan_invoice_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( invoice_item [ key ] , value )
2020-06-15 20:09:24 +02:00
2020-12-23 21:45:16 +01:00
def test_reupgrade_after_plan_status_changed_to_downgrade_at_end_of_cycle ( self ) - > None :
2020-04-23 20:10:15 +02:00
user = self . example_user ( " hamlet " )
self . login_user ( user )
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
with self . assertLogs ( " corporate.stripe " , " INFO " ) as m :
2020-12-17 16:35:33 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
response = self . client_patch (
" /json/billing/plan " , { " status " : CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE }
)
stripe_customer_id = Customer . objects . get ( realm = user . realm ) . id
new_plan = get_current_plan_by_realm ( user . realm )
assert new_plan is not None
expected_log = f " INFO:corporate.stripe:Change plan status: Customer.id: { stripe_customer_id } , CustomerPlan.id: { new_plan . id } , status: { CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE } "
self . assertEqual ( m . output [ 0 ] , expected_log )
self . assert_json_success ( response )
2021-02-12 08:19:30 +01:00
self . assertEqual (
CustomerPlan . objects . first ( ) . status , CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE
)
2021-02-12 08:20:45 +01:00
with self . assertLogs ( " corporate.stripe " , " INFO " ) as m :
2020-12-17 16:35:33 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
response = self . client_patch ( " /json/billing/plan " , { " status " : CustomerPlan . ACTIVE } )
expected_log = f " INFO:corporate.stripe:Change plan status: Customer.id: { stripe_customer_id } , CustomerPlan.id: { new_plan . id } , status: { CustomerPlan . ACTIVE } "
self . assertEqual ( m . output [ 0 ] , expected_log )
self . assert_json_success ( response )
2020-04-23 20:10:15 +02:00
self . assertEqual ( CustomerPlan . objects . first ( ) . status , CustomerPlan . ACTIVE )
2019-04-08 05:16:35 +02:00
@patch ( " stripe.Invoice.create " )
@patch ( " stripe.Invoice.finalize_invoice " )
@patch ( " stripe.InvoiceItem.create " )
def test_downgrade_during_invoicing ( self , * mocks : Mock ) - > None :
# The difference between this test and test_downgrade is that
# CustomerPlan.status is DOWNGRADE_AT_END_OF_CYCLE rather than ENDED
# when we call invoice_plans_as_needed
# This test is essentially checking that we call make_end_of_cycle_updates_if_needed
# during the invoicing process.
user = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( user )
2019-04-08 05:16:35 +02:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
with self . assertLogs ( " corporate.stripe " , " INFO " ) as m :
2021-02-04 11:20:53 +01:00
stripe_customer_id = Customer . objects . get ( realm = user . realm ) . id
new_plan = get_current_plan_by_realm ( user . realm )
assert new_plan is not None
2020-12-17 16:35:33 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
self . client_patch (
" /json/billing/plan " , { " status " : CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE }
)
2021-02-04 11:20:53 +01:00
expected_log = f " INFO:corporate.stripe:Change plan status: Customer.id: { stripe_customer_id } , CustomerPlan.id: { new_plan . id } , status: { CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE } "
self . assertEqual ( m . output [ 0 ] , expected_log )
2019-04-08 05:16:35 +02:00
plan = CustomerPlan . objects . first ( )
self . assertIsNotNone ( plan . next_invoice_date )
self . assertEqual ( plan . status , CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE )
invoice_plans_as_needed ( self . next_year )
plan = CustomerPlan . objects . first ( )
self . assertIsNone ( plan . next_invoice_date )
self . assertEqual ( plan . status , CustomerPlan . ENDED )
2020-12-08 09:25:42 +01:00
def test_downgrade_free_trial ( self ) - > None :
2020-04-23 20:10:15 +02:00
user = self . example_user ( " hamlet " )
2020-05-14 18:21:23 +02:00
free_trial_end_date = self . now + timedelta ( days = 60 )
with self . settings ( FREE_TRIAL_DAYS = 60 ) :
2020-04-23 20:10:15 +02:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2020-04-23 20:10:15 +02:00
plan = CustomerPlan . objects . get ( )
2020-05-14 18:21:23 +02:00
self . assertEqual ( plan . next_invoice_date , free_trial_end_date )
2021-02-12 08:20:45 +01:00
self . assertEqual ( get_realm ( " zulip " ) . plan_type , Realm . STANDARD )
2020-04-23 20:10:15 +02:00
self . assertEqual ( plan . status , CustomerPlan . FREE_TRIAL )
# Add some extra users before the realm is deactivated
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 21 ) :
update_license_ledger_if_needed ( user . realm , self . now )
2021-02-12 08:20:45 +01:00
last_ledger_entry = LicenseLedger . objects . order_by ( " id " ) . last ( )
2020-04-23 20:10:15 +02:00
self . assertEqual ( last_ledger_entry . licenses , 21 )
self . assertEqual ( last_ledger_entry . licenses_at_next_renewal , 21 )
self . login_user ( user )
2020-12-17 16:35:33 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
self . client_patch ( " /json/billing/plan " , { " status " : CustomerPlan . ENDED } )
2020-04-23 20:10:15 +02:00
plan . refresh_from_db ( )
2021-02-12 08:20:45 +01:00
self . assertEqual ( get_realm ( " zulip " ) . plan_type , Realm . LIMITED )
2020-04-23 20:10:15 +02:00
self . assertEqual ( plan . status , CustomerPlan . ENDED )
self . assertEqual ( plan . invoiced_through , last_ledger_entry )
self . assertIsNone ( plan . next_invoice_date )
self . login_user ( user )
response = self . client_get ( " /billing/ " )
2021-02-12 08:19:30 +01:00
self . assert_in_success_response (
[ " Your organization is on the <b>Zulip Free</b> " ] , response
)
2020-04-23 20:10:15 +02:00
# The extra users added in the final month are not charged
with patch ( " corporate.lib.stripe.invoice_plan " ) as mocked :
invoice_plans_as_needed ( self . next_month )
mocked . assert_not_called ( )
# The plan is not renewed after an year
with patch ( " corporate.lib.stripe.invoice_plan " ) as mocked :
invoice_plans_as_needed ( self . next_year )
mocked . assert_not_called ( )
2020-04-03 16:52:35 +02:00
def test_reupgrade_by_billing_admin_after_downgrade ( self , * mocks : Mock ) - > None :
user = self . example_user ( " hamlet " )
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2020-04-03 16:52:35 +02:00
self . login_user ( user )
2021-02-12 08:20:45 +01:00
with self . assertLogs ( " corporate.stripe " , " INFO " ) as m :
2020-12-17 16:35:33 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
self . client_patch (
" /json/billing/plan " , { " status " : CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE }
)
2021-02-04 11:20:53 +01:00
stripe_customer_id = Customer . objects . get ( realm = user . realm ) . id
new_plan = get_current_plan_by_realm ( user . realm )
assert new_plan is not None
expected_log = f " INFO:corporate.stripe:Change plan status: Customer.id: { stripe_customer_id } , CustomerPlan.id: { new_plan . id } , status: { CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE } "
self . assertEqual ( m . output [ 0 ] , expected_log )
2020-04-03 16:52:35 +02:00
2021-02-12 08:19:30 +01:00
with self . assertRaises ( BillingError ) as context , self . assertLogs (
2021-02-12 08:20:45 +01:00
" corporate.stripe " , " WARNING "
2021-02-12 08:19:30 +01:00
) as m :
2020-04-03 16:52:35 +02:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2021-02-04 11:20:53 +01:00
self . assertEqual (
2021-02-12 08:19:30 +01:00
m . output [ 0 ] ,
2021-02-04 11:20:53 +01:00
f " WARNING:corporate.stripe:Customer <Customer <Realm: zulip { user . realm . id } > id> trying to upgrade, but has an active subscription " ,
2021-02-12 08:19:30 +01:00
)
2020-04-03 16:52:35 +02:00
self . assertEqual ( context . exception . description , " subscribing with existing subscription " )
invoice_plans_as_needed ( self . next_year )
response = self . client_get ( " /billing/ " )
self . assert_in_success_response ( [ " Your organization is on the <b>Zulip Free</b> " ] , response )
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . next_year ) :
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2020-04-03 16:52:35 +02:00
self . assertEqual ( Customer . objects . count ( ) , 1 )
self . assertEqual ( CustomerPlan . objects . count ( ) , 2 )
current_plan = CustomerPlan . objects . all ( ) . order_by ( " id " ) . last ( )
next_invoice_date = add_months ( self . next_year , 1 )
self . assertEqual ( current_plan . next_invoice_date , next_invoice_date )
2021-02-12 08:20:45 +01:00
self . assertEqual ( get_realm ( " zulip " ) . plan_type , Realm . STANDARD )
2020-04-03 16:52:35 +02:00
self . assertEqual ( current_plan . status , CustomerPlan . ACTIVE )
old_plan = CustomerPlan . objects . all ( ) . order_by ( " id " ) . first ( )
self . assertEqual ( old_plan . next_invoice_date , None )
self . assertEqual ( old_plan . status , CustomerPlan . ENDED )
2020-12-23 17:08:27 +01:00
@mock_stripe ( )
def test_update_licenses_of_manual_plan_from_billing_page ( self , * mocks : Mock ) - > None :
user = self . example_user ( " hamlet " )
self . login_user ( user )
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
self . upgrade ( invoice = True , licenses = 100 )
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
result = self . client_patch ( " /json/billing/plan " , { " licenses " : 100 } )
self . assert_json_error_contains (
result , " Your plan is already on 100 licenses in the current billing period. "
)
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
result = self . client_patch ( " /json/billing/plan " , { " licenses_at_next_renewal " : 100 } )
self . assert_json_error_contains (
result , " Your plan is already scheduled to renew with 100 licenses. "
)
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
result = self . client_patch ( " /json/billing/plan " , { " licenses " : 50 } )
self . assert_json_error_contains (
result , " You cannot decrease the licenses in the current billing period. "
)
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
result = self . client_patch ( " /json/billing/plan " , { " licenses_at_next_renewal " : 25 } )
self . assert_json_error_contains ( result , " You must invoice for at least 30 users. " )
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
result = self . client_patch ( " /json/billing/plan " , { " licenses " : 2000 } )
self . assert_json_error_contains (
result , " Invoices with more than 1000 licenses can ' t be processed from this page. "
)
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
result = self . client_patch ( " /json/billing/plan " , { " licenses " : 150 } )
self . assert_json_success ( result )
invoice_plans_as_needed ( self . next_year )
stripe_customer = stripe_get_customer (
Customer . objects . get ( realm = user . realm ) . stripe_customer_id
)
[ invoice , _ ] = stripe . Invoice . list ( customer = stripe_customer . id )
invoice_params = {
" amount_due " : ( 8000 * 150 + 8000 * 50 ) ,
" amount_paid " : 0 ,
" attempt_count " : 0 ,
" auto_advance " : True ,
" billing " : " send_invoice " ,
" statement_descriptor " : " Zulip Standard " ,
" status " : " open " ,
" total " : ( 8000 * 150 + 8000 * 50 ) ,
}
for key , value in invoice_params . items ( ) :
self . assertEqual ( invoice . get ( key ) , value )
[ renewal_item , extra_license_item ] = invoice . lines
line_item_params = {
" amount " : 8000 * 150 ,
" description " : " Zulip Standard - renewal " ,
" discountable " : False ,
" period " : {
" end " : datetime_to_timestamp ( self . next_year + timedelta ( days = 365 ) ) ,
" start " : datetime_to_timestamp ( self . next_year ) ,
} ,
" plan " : None ,
" proration " : False ,
" quantity " : 150 ,
}
for key , value in line_item_params . items ( ) :
self . assertEqual ( renewal_item . get ( key ) , value )
line_item_params = {
" amount " : 8000 * 50 ,
" description " : " Additional license (Jan 2, 2012 - Jan 2, 2013) " ,
" discountable " : False ,
" period " : {
" end " : datetime_to_timestamp ( self . next_year ) ,
" start " : datetime_to_timestamp ( self . now ) ,
} ,
" plan " : None ,
" proration " : False ,
" quantity " : 50 ,
}
for key , value in line_item_params . items ( ) :
self . assertEqual ( extra_license_item . get ( key ) , value )
with patch ( " corporate.views.timezone_now " , return_value = self . next_year ) :
result = self . client_patch ( " /json/billing/plan " , { " licenses_at_next_renewal " : 120 } )
self . assert_json_success ( result )
invoice_plans_as_needed ( self . next_year + timedelta ( days = 365 ) )
stripe_customer = stripe_get_customer (
Customer . objects . get ( realm = user . realm ) . stripe_customer_id
)
[ invoice , _ , _ ] = stripe . Invoice . list ( customer = stripe_customer . id )
invoice_params = {
" amount_due " : 8000 * 120 ,
" amount_paid " : 0 ,
" attempt_count " : 0 ,
" auto_advance " : True ,
" billing " : " send_invoice " ,
" statement_descriptor " : " Zulip Standard " ,
" status " : " open " ,
" total " : 8000 * 120 ,
}
for key , value in invoice_params . items ( ) :
self . assertEqual ( invoice . get ( key ) , value )
[ renewal_item ] = invoice . lines
line_item_params = {
" amount " : 8000 * 120 ,
" description " : " Zulip Standard - renewal " ,
" discountable " : False ,
" period " : {
" end " : datetime_to_timestamp ( self . next_year + timedelta ( days = 2 * 365 ) ) ,
" start " : datetime_to_timestamp ( self . next_year + timedelta ( days = 365 ) ) ,
} ,
" plan " : None ,
" proration " : False ,
" quantity " : 120 ,
}
for key , value in line_item_params . items ( ) :
self . assertEqual ( renewal_item . get ( key ) , value )
def test_update_licenses_of_automatic_plan_from_billing_page ( self , * mocks : Mock ) - > None :
user = self . example_user ( " hamlet " )
self . login_user ( user )
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
result = self . client_patch ( " /json/billing/plan " , { " licenses " : 100 } )
self . assert_json_error_contains ( result , " Your plan is on automatic license management. " )
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
result = self . client_patch ( " /json/billing/plan " , { " licenses_at_next_renewal " : 100 } )
self . assert_json_error_contains ( result , " Your plan is on automatic license management. " )
def test_update_plan_with_invalid_status ( self ) - > None :
2021-04-14 15:50:40 +02:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
self . login_user ( self . example_user ( " hamlet " ) )
2020-12-10 18:15:09 +01:00
response = self . client_patch (
" /json/billing/plan " ,
2021-04-14 15:50:40 +02:00
{ " status " : CustomerPlan . NEVER_STARTED } ,
)
self . assert_json_error_contains ( response , " Invalid status " )
2020-12-10 18:15:09 +01:00
def test_update_plan_without_any_params ( self ) - > None :
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
self . login_user ( self . example_user ( " hamlet " ) )
2020-12-17 16:35:33 +01:00
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
response = self . client_patch ( " /json/billing/plan " , { } )
2020-12-10 18:15:09 +01:00
self . assert_json_error_contains ( response , " Nothing to change " )
2020-12-17 16:35:33 +01:00
def test_update_plan_that_which_is_due_for_expiry ( self ) - > None :
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
self . login_user ( self . example_user ( " hamlet " ) )
with self . assertLogs ( " corporate.stripe " , " INFO " ) as m :
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
result = self . client_patch (
" /json/billing/plan " , { " status " : CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE }
)
self . assert_json_success ( result )
self . assertRegexpMatches (
m . output [ 0 ] ,
r " INFO:corporate.stripe:Change plan status: Customer.id: \ d*, CustomerPlan.id: \ d*, status: 2 " ,
)
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . next_year ) :
result = self . client_patch ( " /json/billing/plan " , { " status " : CustomerPlan . ACTIVE } )
self . assert_json_error_contains (
result , " Unable to update the plan. The plan has ended. "
)
def test_update_plan_that_which_is_due_for_replacement ( self ) - > None :
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
self . local_upgrade ( self . seat_count , True , CustomerPlan . MONTHLY , " token " )
self . login_user ( self . example_user ( " hamlet " ) )
with self . assertLogs ( " corporate.stripe " , " INFO " ) as m :
with patch ( " corporate.views.timezone_now " , return_value = self . now ) :
result = self . client_patch (
" /json/billing/plan " , { " status " : CustomerPlan . SWITCH_TO_ANNUAL_AT_END_OF_CYCLE }
)
self . assert_json_success ( result )
self . assertRegexpMatches (
m . output [ 0 ] ,
r " INFO:corporate.stripe:Change plan status: Customer.id: \ d*, CustomerPlan.id: \ d*, status: 4 " ,
)
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . next_month ) :
result = self . client_patch ( " /json/billing/plan " , { } )
self . assert_json_error_contains (
result ,
" Unable to update the plan. The plan has been expired and replaced with a new plan. " ,
)
2020-12-10 18:15:09 +01:00
@patch ( " corporate.lib.stripe.billing_logger.info " )
def test_deactivate_realm ( self , mock_ : Mock ) - > None :
2020-03-20 14:58:38 +01:00
user = self . example_user ( " hamlet " )
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2020-03-20 14:58:38 +01:00
plan = CustomerPlan . objects . get ( )
self . assertEqual ( plan . next_invoice_date , self . next_month )
2021-02-12 08:20:45 +01:00
self . assertEqual ( get_realm ( " zulip " ) . plan_type , Realm . STANDARD )
2020-03-20 14:58:38 +01:00
self . assertEqual ( plan . status , CustomerPlan . ACTIVE )
# Add some extra users before the realm is deactivated
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 20 ) :
update_license_ledger_if_needed ( user . realm , self . now )
2021-02-12 08:20:45 +01:00
last_ledger_entry = LicenseLedger . objects . order_by ( " id " ) . last ( )
2020-03-20 14:58:38 +01:00
self . assertEqual ( last_ledger_entry . licenses , 20 )
self . assertEqual ( last_ledger_entry . licenses_at_next_renewal , 20 )
2021-04-02 17:11:25 +02:00
do_deactivate_realm ( get_realm ( " zulip " ) , acting_user = None )
2020-03-20 14:58:38 +01:00
plan . refresh_from_db ( )
2021-02-12 08:20:45 +01:00
self . assertTrue ( get_realm ( " zulip " ) . deactivated )
self . assertEqual ( get_realm ( " zulip " ) . plan_type , Realm . LIMITED )
2020-03-20 14:58:38 +01:00
self . assertEqual ( plan . status , CustomerPlan . ENDED )
self . assertEqual ( plan . invoiced_through , last_ledger_entry )
self . assertIsNone ( plan . next_invoice_date )
2021-02-12 08:20:45 +01:00
do_reactivate_realm ( get_realm ( " zulip " ) )
2020-04-03 16:17:34 +02:00
self . login_user ( user )
response = self . client_get ( " /billing/ " )
self . assert_in_success_response ( [ " Your organization is on the <b>Zulip Free</b> " ] , response )
2020-03-20 14:58:38 +01:00
# The extra users added in the final month are not charged
with patch ( " corporate.lib.stripe.invoice_plan " ) as mocked :
invoice_plans_as_needed ( self . next_month )
mocked . assert_not_called ( )
# The plan is not renewed after an year
with patch ( " corporate.lib.stripe.invoice_plan " ) as mocked :
invoice_plans_as_needed ( self . next_year )
mocked . assert_not_called ( )
2020-12-08 09:25:42 +01:00
def test_reupgrade_by_billing_admin_after_realm_deactivation ( self ) - > None :
2020-04-03 16:52:35 +02:00
user = self . example_user ( " hamlet " )
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2020-04-03 16:52:35 +02:00
2021-04-02 17:11:25 +02:00
do_deactivate_realm ( get_realm ( " zulip " ) , acting_user = None )
2021-02-12 08:20:45 +01:00
self . assertTrue ( get_realm ( " zulip " ) . deactivated )
do_reactivate_realm ( get_realm ( " zulip " ) )
2020-04-03 16:52:35 +02:00
self . login_user ( user )
response = self . client_get ( " /billing/ " )
self . assert_in_success_response ( [ " Your organization is on the <b>Zulip Free</b> " ] , response )
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2020-04-03 16:52:35 +02:00
self . assertEqual ( Customer . objects . count ( ) , 1 )
self . assertEqual ( CustomerPlan . objects . count ( ) , 2 )
current_plan = CustomerPlan . objects . all ( ) . order_by ( " id " ) . last ( )
self . assertEqual ( current_plan . next_invoice_date , self . next_month )
2021-02-12 08:20:45 +01:00
self . assertEqual ( get_realm ( " zulip " ) . plan_type , Realm . STANDARD )
2020-04-03 16:52:35 +02:00
self . assertEqual ( current_plan . status , CustomerPlan . ACTIVE )
old_plan = CustomerPlan . objects . all ( ) . order_by ( " id " ) . first ( )
self . assertEqual ( old_plan . next_invoice_date , None )
self . assertEqual ( old_plan . status , CustomerPlan . ENDED )
2020-08-13 10:39:25 +02:00
@mock_stripe ( )
def test_void_all_open_invoices ( self , * mock : Mock ) - > None :
iago = self . example_user ( " iago " )
2021-06-18 21:10:45 +02:00
king = self . lear_user ( " king " )
2020-08-13 10:39:25 +02:00
self . assertEqual ( void_all_open_invoices ( iago . realm ) , 0 )
2021-06-18 21:10:45 +02:00
zulip_customer = update_or_create_stripe_customer ( iago )
lear_customer = update_or_create_stripe_customer ( king )
assert zulip_customer . stripe_customer_id
stripe . InvoiceItem . create (
currency = " usd " ,
customer = zulip_customer . stripe_customer_id ,
description = " Zulip standard upgrade " ,
discountable = False ,
unit_amount = 800 ,
quantity = 8 ,
)
stripe_invoice = stripe . Invoice . create (
auto_advance = True ,
billing = " send_invoice " ,
customer = zulip_customer . stripe_customer_id ,
days_until_due = 30 ,
statement_descriptor = " Zulip Standard " ,
)
stripe . Invoice . finalize_invoice ( stripe_invoice )
assert lear_customer . stripe_customer_id
2020-08-13 10:39:25 +02:00
stripe . InvoiceItem . create (
2021-02-12 08:20:45 +01:00
currency = " usd " ,
2021-06-18 21:10:45 +02:00
customer = lear_customer . stripe_customer_id ,
2020-08-13 10:39:25 +02:00
description = " Zulip standard upgrade " ,
discountable = False ,
unit_amount = 800 ,
2021-02-12 08:19:30 +01:00
quantity = 8 ,
2020-08-13 10:39:25 +02:00
)
stripe_invoice = stripe . Invoice . create (
auto_advance = True ,
billing = " send_invoice " ,
2021-06-18 21:10:45 +02:00
customer = lear_customer . stripe_customer_id ,
2020-08-13 10:39:25 +02:00
days_until_due = 30 ,
2021-02-12 08:20:45 +01:00
statement_descriptor = " Zulip Standard " ,
2020-08-13 10:39:25 +02:00
)
stripe . Invoice . finalize_invoice ( stripe_invoice )
self . assertEqual ( void_all_open_invoices ( iago . realm ) , 1 )
2021-06-18 21:10:45 +02:00
invoices = stripe . Invoice . list ( customer = zulip_customer . stripe_customer_id )
self . assert_length ( invoices , 1 )
for invoice in invoices :
self . assertEqual ( invoice . status , " void " )
lear_stripe_customer_id = lear_customer . stripe_customer_id
lear_customer . stripe_customer_id = None
lear_customer . save ( update_fields = [ " stripe_customer_id " ] )
self . assertEqual ( void_all_open_invoices ( king . realm ) , 0 )
lear_customer . stripe_customer_id = lear_stripe_customer_id
lear_customer . save ( update_fields = [ " stripe_customer_id " ] )
self . assertEqual ( void_all_open_invoices ( king . realm ) , 1 )
invoices = stripe . Invoice . list ( customer = lear_customer . stripe_customer_id )
2021-05-17 05:41:32 +02:00
self . assert_length ( invoices , 1 )
2020-08-13 10:39:25 +02:00
for invoice in invoices :
self . assertEqual ( invoice . status , " void " )
2020-08-18 13:48:11 +02:00
def test_update_billing_method_of_current_plan ( self ) - > None :
realm = get_realm ( " zulip " )
2021-02-12 08:20:45 +01:00
customer = Customer . objects . create ( realm = realm , stripe_customer_id = " cus_12345 " )
2021-02-12 08:19:30 +01:00
plan = CustomerPlan . objects . create (
customer = customer ,
status = CustomerPlan . ACTIVE ,
billing_cycle_anchor = timezone_now ( ) ,
billing_schedule = CustomerPlan . ANNUAL ,
tier = CustomerPlan . STANDARD ,
)
2020-08-18 13:48:11 +02:00
self . assertEqual ( plan . charge_automatically , False )
2020-12-04 11:29:02 +01:00
iago = self . example_user ( " iago " )
update_billing_method_of_current_plan ( realm , True , acting_user = iago )
2020-08-18 13:48:11 +02:00
plan . refresh_from_db ( )
self . assertEqual ( plan . charge_automatically , True )
2020-12-04 11:29:02 +01:00
realm_audit_log = RealmAuditLog . objects . filter (
event_type = RealmAuditLog . REALM_BILLING_METHOD_CHANGED
) . last ( )
expected_extra_data = { " charge_automatically " : plan . charge_automatically }
self . assertEqual ( realm_audit_log . acting_user , iago )
self . assertEqual ( realm_audit_log . extra_data , str ( expected_extra_data ) )
2020-08-18 13:48:11 +02:00
2020-12-04 11:29:02 +01:00
update_billing_method_of_current_plan ( realm , False , acting_user = iago )
2020-08-18 13:48:11 +02:00
plan . refresh_from_db ( )
self . assertEqual ( plan . charge_automatically , False )
2020-12-04 11:29:02 +01:00
realm_audit_log = RealmAuditLog . objects . filter (
event_type = RealmAuditLog . REALM_BILLING_METHOD_CHANGED
) . last ( )
expected_extra_data = { " charge_automatically " : plan . charge_automatically }
self . assertEqual ( realm_audit_log . acting_user , iago )
self . assertEqual ( realm_audit_log . extra_data , str ( expected_extra_data ) )
2020-08-18 13:48:11 +02:00
2020-10-14 12:17:03 +02:00
@mock_stripe ( )
def test_customer_has_credit_card_as_default_source ( self , * mocks : Mock ) - > None :
iago = self . example_user ( " iago " )
customer = Customer . objects . create ( realm = iago . realm )
self . assertFalse ( customer_has_credit_card_as_default_source ( customer ) )
customer = do_create_stripe_customer ( iago )
self . assertFalse ( customer_has_credit_card_as_default_source ( customer ) )
customer = do_create_stripe_customer ( iago , stripe_token = stripe_create_token ( ) . id )
self . assertTrue ( customer_has_credit_card_as_default_source ( customer ) )
2021-02-12 08:19:30 +01:00
2018-11-01 11:26:29 +01:00
class RequiresBillingAccessTest ( ZulipTestCase ) :
def setUp ( self ) - > None :
2019-10-19 20:47:00 +02:00
super ( ) . setUp ( )
2018-11-01 11:26:29 +01:00
hamlet = self . example_user ( " hamlet " )
hamlet . is_billing_admin = True
hamlet . save ( update_fields = [ " is_billing_admin " ] )
2021-02-12 08:20:45 +01:00
desdemona = self . example_user ( " desdemona " )
2020-07-14 14:40:39 +02:00
desdemona . role = UserProfile . ROLE_REALM_OWNER
desdemona . save ( update_fields = [ " role " ] )
def test_who_can_access_json_endpoints ( self ) - > None :
# Billing admins have access
2021-02-12 08:20:45 +01:00
self . login_user ( self . example_user ( " hamlet " ) )
2020-07-14 14:40:39 +02:00
with patch ( " corporate.views.do_replace_payment_source " ) as mocked1 :
2021-02-12 08:19:30 +01:00
response = self . client_post (
2021-02-12 08:20:45 +01:00
" /json/billing/sources/change " , { " stripe_token " : orjson . dumps ( " token " ) . decode ( ) }
2021-02-12 08:19:30 +01:00
)
2020-07-14 14:40:39 +02:00
self . assert_json_success ( response )
2020-09-24 00:00:35 +02:00
mocked1 . assert_called_once ( )
2020-07-14 14:40:39 +02:00
# Realm owners have access, even if they are not billing admins
2021-02-12 08:20:45 +01:00
self . login_user ( self . example_user ( " desdemona " ) )
2020-07-14 14:40:39 +02:00
with patch ( " corporate.views.do_replace_payment_source " ) as mocked2 :
2021-02-12 08:19:30 +01:00
response = self . client_post (
2021-02-12 08:20:45 +01:00
" /json/billing/sources/change " , { " stripe_token " : orjson . dumps ( " token " ) . decode ( ) }
2021-02-12 08:19:30 +01:00
)
2020-07-14 14:40:39 +02:00
self . assert_json_success ( response )
2020-09-24 00:00:35 +02:00
mocked2 . assert_called_once ( )
2020-07-14 14:40:39 +02:00
def test_who_cant_access_json_endpoints ( self ) - > None :
2021-02-12 08:19:30 +01:00
def verify_user_cant_access_endpoint (
2020-12-10 18:15:09 +01:00
username : str ,
endpoint : str ,
method : str ,
request_data : Dict [ str , str ] ,
error_message : str ,
2021-02-12 08:19:30 +01:00
) - > None :
2020-12-10 18:15:09 +01:00
2020-07-15 22:18:32 +02:00
self . login_user ( self . example_user ( username ) )
2020-12-10 18:15:09 +01:00
if method == " POST " :
response = self . client_post ( endpoint , request_data )
elif method == " PATCH " :
response = self . client_patch ( endpoint , request_data )
else :
raise AssertionError ( " Invalid method " )
2020-07-15 22:18:32 +02:00
self . assert_json_error_contains ( response , error_message )
2018-11-01 11:26:29 +01:00
2021-02-12 08:19:30 +01:00
verify_user_cant_access_endpoint (
" polonius " ,
" /json/billing/upgrade " ,
2020-12-10 18:15:09 +01:00
" POST " ,
2021-02-12 08:19:30 +01:00
{
2021-02-12 08:20:45 +01:00
" billing_modality " : orjson . dumps ( " charge_automatically " ) . decode ( ) ,
" schedule " : orjson . dumps ( " annual " ) . decode ( ) ,
" signed_seat_count " : orjson . dumps ( " signed count " ) . decode ( ) ,
" salt " : orjson . dumps ( " salt " ) . decode ( ) ,
2021-02-12 08:19:30 +01:00
} ,
" Must be an organization member " ,
)
2018-11-01 11:26:29 +01:00
2021-02-12 08:19:30 +01:00
verify_user_cant_access_endpoint (
" polonius " ,
" /json/billing/sponsorship " ,
2020-12-10 18:15:09 +01:00
" POST " ,
2021-02-12 08:19:30 +01:00
{
2021-02-12 08:20:45 +01:00
" organization-type " : orjson . dumps ( " event " ) . decode ( ) ,
" description " : orjson . dumps ( " event description " ) . decode ( ) ,
" website " : orjson . dumps ( " example.com " ) . decode ( ) ,
2021-02-12 08:19:30 +01:00
} ,
" Must be an organization member " ,
)
2020-07-15 22:18:32 +02:00
for username in [ " cordelia " , " iago " ] :
self . login_user ( self . example_user ( username ) )
2021-02-12 08:19:30 +01:00
verify_user_cant_access_endpoint (
username ,
" /json/billing/sources/change " ,
2020-12-10 18:15:09 +01:00
" POST " ,
2021-02-12 08:20:45 +01:00
{ " stripe_token " : orjson . dumps ( " token " ) . decode ( ) } ,
2021-02-12 08:19:30 +01:00
" Must be a billing administrator or an organization owner " ,
)
2020-07-15 22:18:32 +02:00
2021-02-12 08:19:30 +01:00
verify_user_cant_access_endpoint (
username ,
2020-12-10 18:15:09 +01:00
" /json/billing/plan " ,
" PATCH " ,
2021-02-12 08:20:45 +01:00
{ " status " : orjson . dumps ( 1 ) . decode ( ) } ,
2021-02-12 08:19:30 +01:00
" Must be a billing administrator or an organization owner " ,
)
2018-11-01 11:26:29 +01:00
# Make sure that we are testing all the JSON endpoints
# Quite a hack, but probably fine for now
2021-02-12 08:20:45 +01:00
string_with_all_endpoints = str ( get_resolver ( " corporate.urls " ) . reverse_dict )
2021-02-12 08:19:30 +01:00
json_endpoints = {
2021-02-12 08:20:45 +01:00
word . strip ( " \" ' ()[],$ " ) for word in string_with_all_endpoints . split ( ) if " json/ " in word
2021-02-12 08:19:30 +01:00
}
2021-05-17 05:41:32 +02:00
self . assert_length ( json_endpoints , 4 )
2018-12-15 09:33:25 +01:00
2021-02-12 08:19:30 +01:00
2018-12-15 09:33:25 +01:00
class BillingHelpersTest ( ZulipTestCase ) :
def test_next_month ( self ) - > None :
2020-06-05 06:55:20 +02:00
anchor = datetime ( 2019 , 12 , 31 , 1 , 2 , 3 , tzinfo = timezone . utc )
2018-12-15 09:33:25 +01:00
period_boundaries = [
anchor ,
2020-06-05 06:55:20 +02:00
datetime ( 2020 , 1 , 31 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
2018-12-15 09:33:25 +01:00
# Test that this is the 28th even during leap years
2020-06-05 06:55:20 +02:00
datetime ( 2020 , 2 , 28 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
datetime ( 2020 , 3 , 31 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
datetime ( 2020 , 4 , 30 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
datetime ( 2020 , 5 , 31 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
datetime ( 2020 , 6 , 30 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
datetime ( 2020 , 7 , 31 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
datetime ( 2020 , 8 , 31 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
datetime ( 2020 , 9 , 30 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
datetime ( 2020 , 10 , 31 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
datetime ( 2020 , 11 , 30 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
datetime ( 2020 , 12 , 31 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
datetime ( 2021 , 1 , 31 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
2021-02-12 08:19:30 +01:00
datetime ( 2021 , 2 , 28 , 1 , 2 , 3 , tzinfo = timezone . utc ) ,
]
2018-12-15 09:33:25 +01:00
with self . assertRaises ( AssertionError ) :
add_months ( anchor , - 1 )
2020-03-28 01:25:56 +01:00
# Explicitly test add_months for each value of MAX_DAY_FOR_MONTH and
2018-12-15 09:33:25 +01:00
# for crossing a year boundary
for i , boundary in enumerate ( period_boundaries ) :
self . assertEqual ( add_months ( anchor , i ) , boundary )
# Test next_month for small values
for last , next_ in zip ( period_boundaries [ : - 1 ] , period_boundaries [ 1 : ] ) :
self . assertEqual ( next_month ( anchor , last ) , next_ )
# Test next_month for large values
2021-02-12 08:19:30 +01:00
period_boundaries = [ dt . replace ( year = dt . year + 100 ) for dt in period_boundaries ]
2018-12-15 09:33:25 +01:00
for last , next_ in zip ( period_boundaries [ : - 1 ] , period_boundaries [ 1 : ] ) :
self . assertEqual ( next_month ( anchor , last ) , next_ )
def test_compute_plan_parameters ( self ) - > None :
# TODO: test rounding down microseconds
2020-06-05 06:55:20 +02:00
anchor = datetime ( 2019 , 12 , 31 , 1 , 2 , 3 , tzinfo = timezone . utc )
month_later = datetime ( 2020 , 1 , 31 , 1 , 2 , 3 , tzinfo = timezone . utc )
year_later = datetime ( 2020 , 12 , 31 , 1 , 2 , 3 , tzinfo = timezone . utc )
2018-12-15 09:33:25 +01:00
test_cases = [
# test all possibilities, since there aren't that many
2021-02-12 08:19:30 +01:00
( ( True , CustomerPlan . ANNUAL , None ) , ( anchor , month_later , year_later , 8000 ) ) ,
( ( True , CustomerPlan . ANNUAL , 85 ) , ( anchor , month_later , year_later , 1200 ) ) ,
( ( True , CustomerPlan . MONTHLY , None ) , ( anchor , month_later , month_later , 800 ) ) ,
( ( True , CustomerPlan . MONTHLY , 85 ) , ( anchor , month_later , month_later , 120 ) ) ,
( ( False , CustomerPlan . ANNUAL , None ) , ( anchor , year_later , year_later , 8000 ) ) ,
( ( False , CustomerPlan . ANNUAL , 85 ) , ( anchor , year_later , year_later , 1200 ) ) ,
( ( False , CustomerPlan . MONTHLY , None ) , ( anchor , month_later , month_later , 800 ) ) ,
( ( False , CustomerPlan . MONTHLY , 85 ) , ( anchor , month_later , month_later , 120 ) ) ,
2018-12-15 09:33:25 +01:00
# test exact math of Decimals; 800 * (1 - 87.25) = 101.9999999..
2020-06-23 06:00:56 +02:00
( ( False , CustomerPlan . MONTHLY , 87.25 ) , ( anchor , month_later , month_later , 102 ) ) ,
2018-12-15 09:33:25 +01:00
# test dropping of fractional cents; without the int it's 102.8
2020-06-23 06:00:56 +02:00
( ( False , CustomerPlan . MONTHLY , 87.15 ) , ( anchor , month_later , month_later , 102 ) ) ,
]
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = anchor ) :
2020-06-23 06:00:56 +02:00
for ( automanage_licenses , discount , free_trial ) , output in test_cases :
output_ = compute_plan_parameters (
automanage_licenses ,
discount ,
None if free_trial is None else Decimal ( free_trial ) ,
)
2018-12-15 09:33:25 +01:00
self . assertEqual ( output_ , output )
2020-12-04 12:56:58 +01:00
def test_get_price_per_license ( self ) - > None :
2021-02-12 08:19:30 +01:00
self . assertEqual ( get_price_per_license ( CustomerPlan . STANDARD , CustomerPlan . ANNUAL ) , 8000 )
self . assertEqual ( get_price_per_license ( CustomerPlan . STANDARD , CustomerPlan . MONTHLY ) , 800 )
2020-12-04 12:56:58 +01:00
self . assertEqual (
2021-02-12 08:19:30 +01:00
get_price_per_license (
CustomerPlan . STANDARD , CustomerPlan . MONTHLY , discount = Decimal ( 50 )
) ,
400 ,
2020-12-04 12:56:58 +01:00
)
with self . assertRaises ( AssertionError ) :
get_price_per_license ( CustomerPlan . PLUS , CustomerPlan . MONTHLY )
with self . assertRaisesRegex ( InvalidBillingSchedule , " Unknown billing_schedule: 1000 " ) :
get_price_per_license ( CustomerPlan . STANDARD , 1000 )
2018-12-15 09:33:25 +01:00
def test_update_or_create_stripe_customer_logic ( self ) - > None :
2021-02-12 08:20:45 +01:00
user = self . example_user ( " hamlet " )
2018-12-15 09:33:25 +01:00
# No existing Customer object
2021-02-12 08:19:30 +01:00
with patch (
2021-02-12 08:20:45 +01:00
" corporate.lib.stripe.do_create_stripe_customer " , return_value = " returned "
2021-02-12 08:19:30 +01:00
) as mocked1 :
2021-02-12 08:20:45 +01:00
returned = update_or_create_stripe_customer ( user , stripe_token = " token " )
2020-09-24 00:00:35 +02:00
mocked1 . assert_called_once ( )
2021-02-12 08:20:45 +01:00
self . assertEqual ( returned , " returned " )
2020-03-12 11:39:10 +01:00
2021-02-12 08:20:45 +01:00
customer = Customer . objects . create ( realm = get_realm ( " zulip " ) )
2020-03-12 11:39:10 +01:00
# Customer exists but stripe_customer_id is None
2021-02-12 08:19:30 +01:00
with patch (
2021-02-12 08:20:45 +01:00
" corporate.lib.stripe.do_create_stripe_customer " , return_value = " returned "
2021-02-12 08:19:30 +01:00
) as mocked2 :
2021-02-12 08:20:45 +01:00
returned = update_or_create_stripe_customer ( user , stripe_token = " token " )
2020-09-24 00:00:35 +02:00
mocked2 . assert_called_once ( )
2021-02-12 08:20:45 +01:00
self . assertEqual ( returned , " returned " )
2020-03-12 11:39:10 +01:00
2021-02-12 08:20:45 +01:00
customer . stripe_customer_id = " cus_12345 "
2020-03-12 11:39:10 +01:00
customer . save ( )
2018-12-15 09:33:25 +01:00
# Customer exists, replace payment source
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.do_replace_payment_source " ) as mocked3 :
2021-02-12 08:19:30 +01:00
returned_customer = update_or_create_stripe_customer (
2021-02-12 08:20:45 +01:00
self . example_user ( " hamlet " ) , " token "
2021-02-12 08:19:30 +01:00
)
2020-09-24 00:00:35 +02:00
mocked3 . assert_called_once ( )
2020-03-12 09:05:14 +01:00
self . assertEqual ( returned_customer , customer )
2020-03-12 11:39:10 +01:00
2018-12-15 09:33:25 +01:00
# Customer exists, do nothing
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.do_replace_payment_source " ) as mocked4 :
returned_customer = update_or_create_stripe_customer ( self . example_user ( " hamlet " ) , None )
2020-03-12 11:39:10 +01:00
mocked4 . assert_not_called ( )
2020-03-12 09:05:14 +01:00
self . assertEqual ( returned_customer , customer )
2018-12-28 07:20:30 +01:00
2020-03-23 13:35:04 +01:00
def test_get_customer_by_realm ( self ) - > None :
2021-02-12 08:20:45 +01:00
realm = get_realm ( " zulip " )
2020-03-23 13:35:04 +01:00
self . assertEqual ( get_customer_by_realm ( realm ) , None )
2021-02-12 08:20:45 +01:00
customer = Customer . objects . create ( realm = realm , stripe_customer_id = " cus_12345 " )
2020-03-23 13:35:04 +01:00
self . assertEqual ( get_customer_by_realm ( realm ) , customer )
2020-03-24 14:14:03 +01:00
def test_get_current_plan_by_customer ( self ) - > None :
realm = get_realm ( " zulip " )
2021-02-12 08:20:45 +01:00
customer = Customer . objects . create ( realm = realm , stripe_customer_id = " cus_12345 " )
2020-03-24 14:14:03 +01:00
self . assertEqual ( get_current_plan_by_customer ( customer ) , None )
2021-02-12 08:19:30 +01:00
plan = CustomerPlan . objects . create (
customer = customer ,
status = CustomerPlan . ACTIVE ,
billing_cycle_anchor = timezone_now ( ) ,
billing_schedule = CustomerPlan . ANNUAL ,
tier = CustomerPlan . STANDARD ,
)
2020-03-24 14:14:03 +01:00
self . assertEqual ( get_current_plan_by_customer ( customer ) , plan )
plan . status = CustomerPlan . DOWNGRADE_AT_END_OF_CYCLE
plan . save ( update_fields = [ " status " ] )
self . assertEqual ( get_current_plan_by_customer ( customer ) , plan )
plan . status = CustomerPlan . ENDED
plan . save ( update_fields = [ " status " ] )
self . assertEqual ( get_current_plan_by_customer ( customer ) , None )
plan . status = CustomerPlan . NEVER_STARTED
plan . save ( update_fields = [ " status " ] )
self . assertEqual ( get_current_plan_by_customer ( customer ) , None )
2020-03-24 14:22:27 +01:00
def test_get_current_plan_by_realm ( self ) - > None :
realm = get_realm ( " zulip " )
self . assertEqual ( get_current_plan_by_realm ( realm ) , None )
2021-02-12 08:20:45 +01:00
customer = Customer . objects . create ( realm = realm , stripe_customer_id = " cus_12345 " )
2020-03-24 14:22:27 +01:00
self . assertEqual ( get_current_plan_by_realm ( realm ) , None )
2021-02-12 08:19:30 +01:00
plan = CustomerPlan . objects . create (
customer = customer ,
status = CustomerPlan . ACTIVE ,
billing_cycle_anchor = timezone_now ( ) ,
billing_schedule = CustomerPlan . ANNUAL ,
tier = CustomerPlan . STANDARD ,
)
2020-03-24 14:22:27 +01:00
self . assertEqual ( get_current_plan_by_realm ( realm ) , plan )
2021-06-09 13:46:12 +02:00
def test_get_realms_to_default_discount_dict ( self ) - > None :
Customer . objects . create ( realm = get_realm ( " zulip " ) , stripe_customer_id = " cus_1 " )
lear_customer = Customer . objects . create ( realm = get_realm ( " lear " ) , stripe_customer_id = " cus_2 " )
lear_customer . default_discount = 30
lear_customer . save ( update_fields = [ " default_discount " ] )
zephyr_customer = Customer . objects . create (
realm = get_realm ( " zephyr " ) , stripe_customer_id = " cus_3 "
)
zephyr_customer . default_discount = 0
zephyr_customer . save ( update_fields = [ " default_discount " ] )
self . assertEqual (
get_realms_to_default_discount_dict ( ) ,
{
" lear " : Decimal ( " 30.0000 " ) ,
} ,
)
2020-11-11 14:09:30 +01:00
def test_is_realm_on_free_trial ( self ) - > None :
realm = get_realm ( " zulip " )
self . assertFalse ( is_realm_on_free_trial ( realm ) )
customer = Customer . objects . create ( realm = realm , stripe_customer_id = " cus_12345 " )
plan = CustomerPlan . objects . create (
customer = customer ,
status = CustomerPlan . ACTIVE ,
billing_cycle_anchor = timezone_now ( ) ,
billing_schedule = CustomerPlan . ANNUAL ,
tier = CustomerPlan . STANDARD ,
)
self . assertFalse ( is_realm_on_free_trial ( realm ) )
plan . status = CustomerPlan . FREE_TRIAL
plan . save ( update_fields = [ " status " ] )
self . assertTrue ( is_realm_on_free_trial ( realm ) )
2020-10-14 18:45:57 +02:00
def test_is_sponsored_realm ( self ) - > None :
realm = get_realm ( " zulip " )
self . assertFalse ( is_sponsored_realm ( realm ) )
realm . plan_type = Realm . STANDARD_FREE
realm . save ( )
self . assertTrue ( is_sponsored_realm ( realm ) )
2021-02-12 08:19:30 +01:00
2019-01-27 21:16:02 +01:00
class LicenseLedgerTest ( StripeTestCase ) :
2018-12-28 07:20:30 +01:00
def test_add_plan_renewal_if_needed ( self ) - > None :
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2018-12-28 07:20:30 +01:00
self . assertEqual ( LicenseLedger . objects . count ( ) , 1 )
plan = CustomerPlan . objects . get ( )
# Plan hasn't renewed yet
2019-04-11 00:24:45 +02:00
make_end_of_cycle_updates_if_needed ( plan , self . next_year - timedelta ( days = 1 ) )
2018-12-28 07:20:30 +01:00
self . assertEqual ( LicenseLedger . objects . count ( ) , 1 )
# Plan needs to renew
# TODO: do_deactivate_user for a user, so that licenses_at_next_renewal != licenses
2020-06-15 20:09:24 +02:00
new_plan , ledger_entry = make_end_of_cycle_updates_if_needed ( plan , self . next_year )
self . assertIsNone ( new_plan )
2018-12-28 07:20:30 +01:00
self . assertEqual ( LicenseLedger . objects . count ( ) , 2 )
ledger_params = {
2021-02-12 08:20:45 +01:00
" plan " : plan ,
" is_renewal " : True ,
" event_time " : self . next_year ,
" licenses " : self . seat_count ,
" licenses_at_next_renewal " : self . seat_count ,
2021-02-12 08:19:30 +01:00
}
2018-12-28 07:20:30 +01:00
for key , value in ledger_params . items ( ) :
self . assertEqual ( getattr ( ledger_entry , key ) , value )
# Plan needs to renew, but we already added the plan_renewal ledger entry
2019-04-11 00:24:45 +02:00
make_end_of_cycle_updates_if_needed ( plan , self . next_year + timedelta ( days = 1 ) )
2018-12-28 07:20:30 +01:00
self . assertEqual ( LicenseLedger . objects . count ( ) , 2 )
2019-01-26 02:36:37 +01:00
def test_update_license_ledger_if_needed ( self ) - > None :
2021-02-12 08:20:45 +01:00
realm = get_realm ( " zulip " )
2019-01-26 02:36:37 +01:00
# Test no Customer
update_license_ledger_if_needed ( realm , self . now )
self . assertFalse ( LicenseLedger . objects . exists ( ) )
# Test plan not automanaged
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count + 1 , False , CustomerPlan . ANNUAL , " token " )
2020-12-25 19:12:30 +01:00
plan = CustomerPlan . objects . get ( )
2019-01-26 02:36:37 +01:00
self . assertEqual ( LicenseLedger . objects . count ( ) , 1 )
2020-12-25 19:12:30 +01:00
self . assertEqual ( plan . licenses ( ) , self . seat_count + 1 )
2020-12-30 18:57:35 +01:00
self . assertEqual ( plan . licenses_at_next_renewal ( ) , self . seat_count + 1 )
2019-01-26 02:36:37 +01:00
update_license_ledger_if_needed ( realm , self . now )
self . assertEqual ( LicenseLedger . objects . count ( ) , 1 )
# Test no active plan
plan . automanage_licenses = True
plan . status = CustomerPlan . ENDED
2021-02-12 08:20:45 +01:00
plan . save ( update_fields = [ " automanage_licenses " , " status " ] )
2019-01-26 02:36:37 +01:00
update_license_ledger_if_needed ( realm , self . now )
self . assertEqual ( LicenseLedger . objects . count ( ) , 1 )
# Test update needed
plan . status = CustomerPlan . ACTIVE
2021-02-12 08:20:45 +01:00
plan . save ( update_fields = [ " status " ] )
2019-01-26 02:36:37 +01:00
update_license_ledger_if_needed ( realm , self . now )
self . assertEqual ( LicenseLedger . objects . count ( ) , 2 )
def test_update_license_ledger_for_automanaged_plan ( self ) - > None :
2021-02-12 08:20:45 +01:00
realm = get_realm ( " zulip " )
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2019-01-26 02:36:37 +01:00
plan = CustomerPlan . objects . first ( )
2020-12-25 19:12:30 +01:00
self . assertEqual ( plan . licenses ( ) , self . seat_count )
2020-12-30 18:57:35 +01:00
self . assertEqual ( plan . licenses_at_next_renewal ( ) , self . seat_count )
2019-01-26 02:36:37 +01:00
# Simple increase
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 23 ) :
2019-01-26 02:36:37 +01:00
update_license_ledger_for_automanaged_plan ( realm , plan , self . now )
2020-12-25 19:12:30 +01:00
self . assertEqual ( plan . licenses ( ) , 23 )
2020-12-30 18:57:35 +01:00
self . assertEqual ( plan . licenses_at_next_renewal ( ) , 23 )
2019-01-26 02:36:37 +01:00
# Decrease
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 20 ) :
2019-01-26 02:36:37 +01:00
update_license_ledger_for_automanaged_plan ( realm , plan , self . now )
2020-12-25 19:12:30 +01:00
self . assertEqual ( plan . licenses ( ) , 23 )
2020-12-30 18:57:35 +01:00
self . assertEqual ( plan . licenses_at_next_renewal ( ) , 20 )
2019-01-26 02:36:37 +01:00
# Increase, but not past high watermark
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 21 ) :
2019-01-26 02:36:37 +01:00
update_license_ledger_for_automanaged_plan ( realm , plan , self . now )
2020-12-25 19:12:30 +01:00
self . assertEqual ( plan . licenses ( ) , 23 )
2020-12-30 18:57:35 +01:00
self . assertEqual ( plan . licenses_at_next_renewal ( ) , 21 )
2019-01-26 02:36:37 +01:00
# Increase, but after renewal date, and below last year's high watermark
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = 22 ) :
2021-02-12 08:19:30 +01:00
update_license_ledger_for_automanaged_plan (
realm , plan , self . next_year + timedelta ( seconds = 1 )
)
2020-12-25 19:12:30 +01:00
self . assertEqual ( plan . licenses ( ) , 22 )
2020-12-30 18:57:35 +01:00
self . assertEqual ( plan . licenses_at_next_renewal ( ) , 22 )
2021-02-12 08:19:30 +01:00
ledger_entries = list (
LicenseLedger . objects . values_list (
2021-02-12 08:20:45 +01:00
" is_renewal " , " event_time " , " licenses " , " licenses_at_next_renewal "
) . order_by ( " id " )
2021-02-12 08:19:30 +01:00
)
self . assertEqual (
ledger_entries ,
[
( True , self . now , self . seat_count , self . seat_count ) ,
( False , self . now , 23 , 23 ) ,
( False , self . now , 23 , 20 ) ,
( False , self . now , 23 , 21 ) ,
( True , self . next_year , 21 , 21 ) ,
( False , self . next_year + timedelta ( seconds = 1 ) , 22 , 22 ) ,
] ,
)
2019-01-26 02:36:37 +01:00
2020-12-30 18:56:57 +01:00
def test_update_license_ledger_for_manual_plan ( self ) - > None :
realm = get_realm ( " zulip " )
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
self . local_upgrade ( self . seat_count + 1 , False , CustomerPlan . ANNUAL , " token " )
plan = get_current_plan_by_realm ( realm )
assert plan is not None
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = self . seat_count ) :
update_license_ledger_for_manual_plan ( plan , self . now , licenses = self . seat_count + 3 )
self . assertEqual ( plan . licenses ( ) , self . seat_count + 3 )
self . assertEqual ( plan . licenses_at_next_renewal ( ) , self . seat_count + 3 )
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = self . seat_count ) :
with self . assertRaises ( AssertionError ) :
update_license_ledger_for_manual_plan ( plan , self . now , licenses = self . seat_count )
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = self . seat_count ) :
update_license_ledger_for_manual_plan (
plan , self . now , licenses_at_next_renewal = self . seat_count
)
self . assertEqual ( plan . licenses ( ) , self . seat_count + 3 )
self . assertEqual ( plan . licenses_at_next_renewal ( ) , self . seat_count )
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = self . seat_count ) :
with self . assertRaises ( AssertionError ) :
update_license_ledger_for_manual_plan (
plan , self . now , licenses_at_next_renewal = self . seat_count - 1
)
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = self . seat_count ) :
update_license_ledger_for_manual_plan ( plan , self . now , licenses = self . seat_count + 10 )
self . assertEqual ( plan . licenses ( ) , self . seat_count + 10 )
self . assertEqual ( plan . licenses_at_next_renewal ( ) , self . seat_count + 10 )
make_end_of_cycle_updates_if_needed ( plan , self . next_year )
self . assertEqual ( plan . licenses ( ) , self . seat_count + 10 )
ledger_entries = list (
LicenseLedger . objects . values_list (
" is_renewal " , " event_time " , " licenses " , " licenses_at_next_renewal "
) . order_by ( " id " )
)
self . assertEqual (
ledger_entries ,
[
( True , self . now , self . seat_count + 1 , self . seat_count + 1 ) ,
( False , self . now , self . seat_count + 3 , self . seat_count + 3 ) ,
( False , self . now , self . seat_count + 3 , self . seat_count ) ,
( False , self . now , self . seat_count + 10 , self . seat_count + 10 ) ,
( True , self . next_year , self . seat_count + 10 , self . seat_count + 10 ) ,
] ,
)
with self . assertRaises ( AssertionError ) :
update_license_ledger_for_manual_plan ( plan , self . now )
2019-01-26 02:36:37 +01:00
def test_user_changes ( self ) - > None :
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2021-02-06 14:27:06 +01:00
user = do_create_user ( " email " , " password " , get_realm ( " zulip " ) , " name " , acting_user = None )
2021-03-27 06:02:12 +01:00
do_deactivate_user ( user , acting_user = None )
2021-03-27 05:42:18 +01:00
do_reactivate_user ( user , acting_user = None )
2019-01-26 02:36:37 +01:00
# Not a proper use of do_activate_user, but fine for this test
2021-02-06 17:02:03 +01:00
do_activate_user ( user , acting_user = None )
2021-02-12 08:19:30 +01:00
ledger_entries = list (
LicenseLedger . objects . values_list (
2021-02-12 08:20:45 +01:00
" is_renewal " , " licenses " , " licenses_at_next_renewal "
) . order_by ( " id " )
2021-02-12 08:19:30 +01:00
)
self . assertEqual (
ledger_entries ,
[
( True , self . seat_count , self . seat_count ) ,
( False , self . seat_count + 1 , self . seat_count + 1 ) ,
( False , self . seat_count + 1 , self . seat_count ) ,
( False , self . seat_count + 1 , self . seat_count + 1 ) ,
( False , self . seat_count + 1 , self . seat_count + 1 ) ,
] ,
)
2019-01-28 22:57:29 +01:00
class InvoiceTest ( StripeTestCase ) :
def test_invoicing_status_is_started ( self ) - > None :
2021-02-12 08:20:45 +01:00
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2019-01-28 22:57:29 +01:00
plan = CustomerPlan . objects . first ( )
plan . invoicing_status = CustomerPlan . STARTED
2021-02-12 08:20:45 +01:00
plan . save ( update_fields = [ " invoicing_status " ] )
2019-01-28 22:57:29 +01:00
with self . assertRaises ( NotImplementedError ) :
invoice_plan ( CustomerPlan . objects . first ( ) , self . now )
2021-06-18 21:10:45 +02:00
def test_invoice_plan_without_stripe_customer ( self ) - > None :
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL )
plan = get_current_plan_by_realm ( get_realm ( " zulip " ) )
assert plan and plan . customer
plan . customer . stripe_customer_id = None
plan . customer . save ( update_fields = [ " stripe_customer_id " ] )
with self . assertRaisesRegex (
BillingError , " Realm zulip has a paid plan without a Stripe customer "
) :
invoice_plan ( plan , timezone_now ( ) )
2019-01-28 22:57:29 +01:00
@mock_stripe ( )
def test_invoice_plan ( self , * mocks : Mock ) - > None :
user = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( user )
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2019-01-28 22:57:29 +01:00
self . upgrade ( )
# Increase
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = self . seat_count + 3 ) :
update_license_ledger_if_needed ( get_realm ( " zulip " ) , self . now + timedelta ( days = 100 ) )
2019-01-28 22:57:29 +01:00
# Decrease
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = self . seat_count ) :
update_license_ledger_if_needed ( get_realm ( " zulip " ) , self . now + timedelta ( days = 200 ) )
2019-01-28 22:57:29 +01:00
# Increase, but not past high watermark
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = self . seat_count + 1 ) :
update_license_ledger_if_needed ( get_realm ( " zulip " ) , self . now + timedelta ( days = 300 ) )
2019-01-28 22:57:29 +01:00
# Increase, but after renewal date, and below last year's high watermark
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = self . seat_count + 2 ) :
update_license_ledger_if_needed ( get_realm ( " zulip " ) , self . now + timedelta ( days = 400 ) )
2019-01-28 22:57:29 +01:00
# Increase, but after event_time
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.get_latest_seat_count " , return_value = self . seat_count + 3 ) :
update_license_ledger_if_needed ( get_realm ( " zulip " ) , self . now + timedelta ( days = 500 ) )
2019-01-28 22:57:29 +01:00
plan = CustomerPlan . objects . first ( )
invoice_plan ( plan , self . now + timedelta ( days = 400 ) )
2020-09-02 07:55:39 +02:00
[ invoice0 , invoice1 ] = stripe . Invoice . list ( customer = plan . customer . stripe_customer_id )
self . assertIsNotNone ( invoice0 . status_transitions . finalized_at )
[ item0 , item1 , item2 ] = invoice0 . lines
2019-01-28 22:57:29 +01:00
line_item_params = {
2021-02-12 08:20:45 +01:00
" amount " : int ( 8000 * ( 1 - ( ( 400 - 366 ) / 365 ) ) + 0.5 ) ,
" description " : " Additional license (Feb 5, 2013 - Jan 2, 2014) " ,
" discountable " : False ,
" period " : {
" start " : datetime_to_timestamp ( self . now + timedelta ( days = 400 ) ) ,
" end " : datetime_to_timestamp ( self . now + timedelta ( days = 2 * 365 + 1 ) ) ,
2021-02-12 08:19:30 +01:00
} ,
2021-02-12 08:20:45 +01:00
" quantity " : 1 ,
2021-02-12 08:19:30 +01:00
}
2019-01-28 22:57:29 +01:00
for key , value in line_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( item0 . get ( key ) , value )
2019-01-28 22:57:29 +01:00
line_item_params = {
2021-02-12 08:20:45 +01:00
" amount " : 8000 * ( self . seat_count + 1 ) ,
" description " : " Zulip Standard - renewal " ,
" discountable " : False ,
" period " : {
" start " : datetime_to_timestamp ( self . now + timedelta ( days = 366 ) ) ,
" end " : datetime_to_timestamp ( self . now + timedelta ( days = 2 * 365 + 1 ) ) ,
2021-02-12 08:19:30 +01:00
} ,
2021-02-12 08:20:45 +01:00
" quantity " : ( self . seat_count + 1 ) ,
2021-02-12 08:19:30 +01:00
}
2019-01-28 22:57:29 +01:00
for key , value in line_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( item1 . get ( key ) , value )
2019-01-28 22:57:29 +01:00
line_item_params = {
2021-02-12 08:20:45 +01:00
" amount " : 3 * int ( 8000 * ( 366 - 100 ) / 366 + 0.5 ) ,
" description " : " Additional license (Apr 11, 2012 - Jan 2, 2013) " ,
" discountable " : False ,
" period " : {
" start " : datetime_to_timestamp ( self . now + timedelta ( days = 100 ) ) ,
" end " : datetime_to_timestamp ( self . now + timedelta ( days = 366 ) ) ,
2021-02-12 08:19:30 +01:00
} ,
2021-02-12 08:20:45 +01:00
" quantity " : 3 ,
2021-02-12 08:19:30 +01:00
}
2019-01-28 22:57:29 +01:00
for key , value in line_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( item2 . get ( key ) , value )
2019-01-28 22:57:29 +01:00
@mock_stripe ( )
def test_fixed_price_plans ( self , * mocks : Mock ) - > None :
# Also tests charge_automatically=False
user = self . example_user ( " hamlet " )
2020-03-06 18:40:46 +01:00
self . login_user ( user )
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
2019-01-28 22:57:29 +01:00
self . upgrade ( invoice = True )
plan = CustomerPlan . objects . first ( )
plan . fixed_price = 100
plan . price_per_license = 0
2021-02-12 08:20:45 +01:00
plan . save ( update_fields = [ " fixed_price " , " price_per_license " ] )
2019-01-28 22:57:29 +01:00
invoice_plan ( plan , self . next_year )
2020-09-02 07:55:39 +02:00
[ invoice0 , invoice1 ] = stripe . Invoice . list ( customer = plan . customer . stripe_customer_id )
2021-02-12 08:20:45 +01:00
self . assertEqual ( invoice0 . billing , " send_invoice " )
2020-09-02 07:55:39 +02:00
[ item ] = invoice0 . lines
2019-01-28 22:57:29 +01:00
line_item_params = {
2021-02-12 08:20:45 +01:00
" amount " : 100 ,
" description " : " Zulip Standard - renewal " ,
" discountable " : False ,
" period " : {
" start " : datetime_to_timestamp ( self . next_year ) ,
" end " : datetime_to_timestamp ( self . next_year + timedelta ( days = 365 ) ) ,
2021-02-12 08:19:30 +01:00
} ,
2021-02-12 08:20:45 +01:00
" quantity " : 1 ,
2021-02-12 08:19:30 +01:00
}
2019-01-28 22:57:29 +01:00
for key , value in line_item_params . items ( ) :
2020-09-02 07:55:39 +02:00
self . assertEqual ( item . get ( key ) , value )
2019-01-28 22:57:29 +01:00
def test_no_invoice_needed ( self ) - > None :
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2019-01-28 22:57:29 +01:00
plan = CustomerPlan . objects . first ( )
self . assertEqual ( plan . next_invoice_date , self . next_month )
# Test this doesn't make any calls to stripe.Invoice or stripe.InvoiceItem
invoice_plan ( plan , self . next_month )
plan = CustomerPlan . objects . first ( )
# Test that we still update next_invoice_date
self . assertEqual ( plan . next_invoice_date , self . next_month + timedelta ( days = 29 ) )
def test_invoice_plans_as_needed ( self ) - > None :
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.timezone_now " , return_value = self . now ) :
self . local_upgrade ( self . seat_count , True , CustomerPlan . ANNUAL , " token " )
2019-01-28 22:57:29 +01:00
plan = CustomerPlan . objects . first ( )
self . assertEqual ( plan . next_invoice_date , self . next_month )
# Test nothing needed to be done
2021-02-12 08:20:45 +01:00
with patch ( " corporate.lib.stripe.invoice_plan " ) as mocked :
2019-01-28 22:57:29 +01:00
invoice_plans_as_needed ( self . next_month - timedelta ( days = 1 ) )
mocked . assert_not_called ( )
# Test something needing to be done
invoice_plans_as_needed ( self . next_month )
plan = CustomerPlan . objects . first ( )
self . assertEqual ( plan . next_invoice_date , self . next_month + timedelta ( days = 29 ) )
2021-06-03 12:20:31 +02:00
class TestTestClasses ( ZulipTestCase ) :
def test_subscribe_realm_to_manual_license_management_plan ( self ) - > None :
realm = get_realm ( " zulip " )
plan , ledger = self . subscribe_realm_to_manual_license_management_plan (
realm , 50 , 60 , CustomerPlan . ANNUAL
)
plan . refresh_from_db ( )
self . assertEqual ( plan . automanage_licenses , False )
self . assertEqual ( plan . billing_schedule , CustomerPlan . ANNUAL )
self . assertEqual ( plan . tier , CustomerPlan . STANDARD )
self . assertEqual ( plan . licenses ( ) , 50 )
self . assertEqual ( plan . licenses_at_next_renewal ( ) , 60 )
ledger . refresh_from_db ( )
self . assertEqual ( ledger . plan , plan )
self . assertEqual ( ledger . licenses , 50 )
self . assertEqual ( ledger . licenses_at_next_renewal , 60 )
realm . refresh_from_db ( )
self . assertEqual ( realm . plan_type , Realm . STANDARD )
def test_subscribe_realm_to_monthly_plan_on_manual_license_management ( self ) - > None :
realm = get_realm ( " zulip " )
plan , ledger = self . subscribe_realm_to_monthly_plan_on_manual_license_management (
realm , 20 , 30
)
plan . refresh_from_db ( )
self . assertEqual ( plan . automanage_licenses , False )
self . assertEqual ( plan . billing_schedule , CustomerPlan . MONTHLY )
self . assertEqual ( plan . tier , CustomerPlan . STANDARD )
self . assertEqual ( plan . licenses ( ) , 20 )
self . assertEqual ( plan . licenses_at_next_renewal ( ) , 30 )
ledger . refresh_from_db ( )
self . assertEqual ( ledger . plan , plan )
self . assertEqual ( ledger . licenses , 20 )
self . assertEqual ( ledger . licenses_at_next_renewal , 30 )
realm . refresh_from_db ( )
self . assertEqual ( realm . plan_type , Realm . STANDARD )