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
|
2021-06-11 12:53:45 +02:00
|
|
|
import random
|
2018-08-08 16:35:33 +02:00
|
|
|
import re
|
2018-10-17 08:23:13 +02:00
|
|
|
import sys
|
2023-11-02 17:44:02 +01:00
|
|
|
import typing
|
2021-12-22 14:37:12 +01:00
|
|
|
import uuid
|
2021-08-03 06:29:32 +02:00
|
|
|
from dataclasses import dataclass
|
2020-06-11 00:54:34 +02:00
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from decimal import Decimal
|
|
|
|
from functools import wraps
|
2022-06-08 04:52:09 +02:00
|
|
|
from typing import (
|
|
|
|
TYPE_CHECKING,
|
|
|
|
Any,
|
|
|
|
Callable,
|
|
|
|
Dict,
|
|
|
|
List,
|
|
|
|
Mapping,
|
|
|
|
Optional,
|
|
|
|
Sequence,
|
|
|
|
Tuple,
|
|
|
|
TypeVar,
|
|
|
|
)
|
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
|
2022-01-22 06:50:24 +01:00
|
|
|
import stripe.util
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.conf import settings
|
2018-07-13 17:34:39 +02:00
|
|
|
from django.core import signing
|
2020-06-11 00:54:34 +02:00
|
|
|
from django.urls.resolvers import get_resolver
|
2021-08-29 15:33:29 +02:00
|
|
|
from django.utils.crypto import get_random_string
|
2020-03-24 14:14:03 +01:00
|
|
|
from django.utils.timezone import now as timezone_now
|
2023-10-12 19:43:45 +02:00
|
|
|
from typing_extensions import ParamSpec, override
|
2018-07-13 17:34:39 +02:00
|
|
|
|
2023-10-30 14:32:02 +01:00
|
|
|
from corporate.lib.analytics import get_realms_with_default_discount_dict
|
2020-06-11 00:54:34 +02:00
|
|
|
from corporate.lib.stripe import (
|
2021-06-11 12:53:45 +02:00
|
|
|
DEFAULT_INVOICE_DAYS_UNTIL_DUE,
|
2020-06-11 00:54:34 +02:00
|
|
|
MAX_INVOICED_LICENSES,
|
|
|
|
MIN_INVOICED_LICENSES,
|
2021-09-07 17:53:27 +02:00
|
|
|
STRIPE_API_VERSION,
|
2023-11-02 17:44:02 +01:00
|
|
|
AuditLogEventType,
|
2020-06-11 00:54:34 +02:00
|
|
|
BillingError,
|
2023-11-02 17:44:02 +01:00
|
|
|
BillingSessionAuditLogEventError,
|
2022-11-17 09:30:48 +01:00
|
|
|
InvalidBillingScheduleError,
|
|
|
|
InvalidTierError,
|
2023-10-26 14:11:43 +02:00
|
|
|
RealmBillingSession,
|
2020-06-11 00:54:34 +02:00
|
|
|
StripeCardError,
|
|
|
|
add_months,
|
|
|
|
catch_stripe_errors,
|
|
|
|
compute_plan_parameters,
|
2021-08-29 15:33:29 +02:00
|
|
|
customer_has_credit_card_as_default_payment_method,
|
2021-12-01 17:31:08 +01:00
|
|
|
do_change_remote_server_plan_type,
|
2021-12-15 18:53:58 +01:00
|
|
|
do_deactivate_remote_server,
|
2021-06-11 12:53:45 +02:00
|
|
|
downgrade_small_realms_behind_on_payments_as_needed,
|
2020-06-11 00:54:34 +02:00
|
|
|
get_latest_seat_count,
|
2021-09-21 21:21:03 +02:00
|
|
|
get_plan_renewal_or_end_date,
|
2020-12-04 12:56:58 +01:00
|
|
|
get_price_per_license,
|
2020-06-11 00:54:34 +02:00
|
|
|
invoice_plan,
|
|
|
|
invoice_plans_as_needed,
|
2021-08-29 15:33:29 +02:00
|
|
|
is_free_trial_offer_enabled,
|
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,
|
2021-08-29 15:33:29 +02:00
|
|
|
stripe_customer_has_credit_card_as_default_payment_method,
|
2020-06-11 00:54:34 +02:00
|
|
|
stripe_get_customer,
|
2021-09-21 21:21:03 +02:00
|
|
|
switch_realm_from_standard_to_plus_plan,
|
2020-06-11 00:54:34 +02:00
|
|
|
unsign_string,
|
|
|
|
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,
|
2020-08-13 10:39:25 +02:00
|
|
|
void_all_open_invoices,
|
2020-06-11 00:54:34 +02:00
|
|
|
)
|
2023-11-02 15:23:35 +01:00
|
|
|
from corporate.lib.support import (
|
|
|
|
approve_realm_sponsorship,
|
|
|
|
attach_discount_to_realm,
|
|
|
|
get_discount_for_realm,
|
2023-11-02 18:42:04 +01:00
|
|
|
update_realm_billing_method,
|
2023-11-02 18:17:08 +01:00
|
|
|
update_realm_sponsorship_status,
|
2023-11-02 15:23:35 +01:00
|
|
|
)
|
2020-06-11 00:54:34 +02:00
|
|
|
from corporate.models import (
|
|
|
|
Customer,
|
|
|
|
CustomerPlan,
|
2021-08-29 15:33:29 +02:00
|
|
|
Event,
|
2020-06-11 00:54:34 +02:00
|
|
|
LicenseLedger,
|
2021-08-29 15:33:29 +02:00
|
|
|
PaymentIntent,
|
|
|
|
Session,
|
2021-07-09 19:56:55 +02:00
|
|
|
ZulipSponsorshipRequest,
|
2020-06-11 00:54:34 +02:00
|
|
|
get_current_plan_by_customer,
|
|
|
|
get_current_plan_by_realm,
|
|
|
|
get_customer_by_realm,
|
|
|
|
)
|
2022-04-14 23:58:15 +02:00
|
|
|
from zerver.actions.create_realm import do_create_realm
|
2022-04-14 23:53:15 +02:00
|
|
|
from zerver.actions.create_user import (
|
2021-07-09 02:27:06 +02:00
|
|
|
do_activate_mirror_dummy_user,
|
2020-06-11 00:54:34 +02:00
|
|
|
do_create_user,
|
|
|
|
do_reactivate_user,
|
|
|
|
)
|
2022-04-14 23:57:15 +02:00
|
|
|
from zerver.actions.realm_settings import do_deactivate_realm, do_reactivate_realm
|
2022-04-14 23:53:15 +02:00
|
|
|
from zerver.actions.users import do_deactivate_user
|
2018-03-31 04:13:44 +02:00
|
|
|
from zerver.lib.test_classes import ZulipTestCase
|
2020-06-11 00:54:34 +02:00
|
|
|
from zerver.lib.timestamp import datetime_to_timestamp, timestamp_to_datetime
|
2021-07-25 16:31:12 +02:00
|
|
|
from zerver.lib.utils import assert_is_not_none
|
2021-06-29 07:56:46 +02:00
|
|
|
from zerver.models import (
|
|
|
|
Message,
|
|
|
|
Realm,
|
|
|
|
RealmAuditLog,
|
|
|
|
Recipient,
|
|
|
|
UserProfile,
|
|
|
|
get_realm,
|
|
|
|
get_system_bot,
|
|
|
|
)
|
2021-12-01 17:31:08 +01:00
|
|
|
from zilencer.models import RemoteZulipServer, RemoteZulipServerAuditLog
|
2018-03-31 04:13:44 +02:00
|
|
|
|
2022-06-08 04:52:09 +02:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
|
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
CallableT = TypeVar("CallableT", bound=Callable[..., Any])
|
2022-04-13 16:42:42 +02:00
|
|
|
ParamT = ParamSpec("ParamT")
|
|
|
|
ReturnT = TypeVar("ReturnT")
|
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
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
|
|
|
|
def create_payment_method(card_number: str) -> stripe.PaymentMethod:
|
|
|
|
return stripe.PaymentMethod.create(
|
|
|
|
type="card",
|
2018-10-17 08:23:13 +02:00
|
|
|
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",
|
2021-08-29 15:33:29 +02:00
|
|
|
},
|
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:
|
2022-10-08 07:04:09 +02:00
|
|
|
error_dict = {**vars(e), "headers": dict(e.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
|
|
|
)
|
2022-10-08 07:04:09 +02:00
|
|
|
raise
|
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:
|
2023-06-20 22:52:31 +02:00
|
|
|
requester = stripe.api_requestor.APIRequestor()
|
2018-10-29 07:36:50 +01:00
|
|
|
# This function will raise the relevant StripeError according to the fixture
|
2023-06-20 22:52:31 +02:00
|
|
|
requester.interpret_response(
|
2021-02-12 08:19:30 +01:00
|
|
|
fixture["http_body"], fixture["http_status"], fixture["headers"]
|
|
|
|
)
|
2022-03-16 02:08:26 +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-07-20 13:28:54 +02:00
|
|
|
("test", 12),
|
2021-02-12 08:20:45 +01:00
|
|
|
("cus", 14),
|
2021-07-20 13:28:54 +02:00
|
|
|
("prod", 14),
|
|
|
|
("req", 14),
|
2021-02-12 08:20:45 +01:00
|
|
|
("si", 14),
|
|
|
|
("sli", 14),
|
2021-07-20 13:28:54 +02:00
|
|
|
("sub", 14),
|
|
|
|
("acct", 16),
|
2021-02-12 08:20:45 +01:00
|
|
|
("card", 24),
|
|
|
|
("ch", 24),
|
|
|
|
("ii", 24),
|
2021-07-20 13:28:54 +02:00
|
|
|
("il", 24),
|
|
|
|
("in", 24),
|
|
|
|
("pi", 24),
|
|
|
|
("price", 24),
|
2021-02-12 08:20:45 +01:00
|
|
|
("src", 24),
|
2021-07-20 13:28:54 +02:00
|
|
|
("src_client_secret", 24),
|
|
|
|
("tok", 24),
|
|
|
|
("txn", 24),
|
2021-02-12 08:20:45 +01:00
|
|
|
("invst", 26),
|
|
|
|
("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
|
|
|
|
2023-01-05 07:26:31 +01:00
|
|
|
normalized_values: Dict[str, Dict[str, str]] = {pattern: {} for pattern in pattern_translations}
|
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 [
|
2021-08-29 15:33:29 +02:00
|
|
|
"checkout.Session.create",
|
|
|
|
"checkout.Session.list",
|
2021-02-12 08:19:30 +01:00
|
|
|
"Charge.create",
|
|
|
|
"Charge.list",
|
|
|
|
"Coupon.create",
|
|
|
|
"Customer.create",
|
2021-09-21 21:21:03 +02:00
|
|
|
"Customer.create_balance_transaction",
|
2021-10-30 18:54:33 +02:00
|
|
|
"Customer.list_balance_transactions",
|
2021-02-12 08:19:30 +01:00
|
|
|
"Customer.retrieve",
|
|
|
|
"Customer.save",
|
2021-08-29 15:33:29 +02:00
|
|
|
"Customer.list",
|
|
|
|
"Customer.modify",
|
|
|
|
"Event.list",
|
2021-02-12 08:19:30 +01:00
|
|
|
"Invoice.create",
|
|
|
|
"Invoice.finalize_invoice",
|
|
|
|
"Invoice.list",
|
|
|
|
"Invoice.pay",
|
2021-06-11 12:53:45 +02:00
|
|
|
"Invoice.refresh",
|
2021-02-12 08:19:30 +01:00
|
|
|
"Invoice.upcoming",
|
|
|
|
"Invoice.void_invoice",
|
|
|
|
"InvoiceItem.create",
|
|
|
|
"InvoiceItem.list",
|
2021-08-29 15:33:29 +02:00
|
|
|
"PaymentIntent.confirm",
|
|
|
|
"PaymentIntent.create",
|
|
|
|
"PaymentIntent.list",
|
|
|
|
"PaymentIntent.retrieve",
|
|
|
|
"PaymentMethod.attach",
|
|
|
|
"PaymentMethod.create",
|
|
|
|
"PaymentMethod.detach",
|
|
|
|
"PaymentMethod.list",
|
2021-02-12 08:19:30 +01:00
|
|
|
"Plan.create",
|
|
|
|
"Product.create",
|
2021-08-29 15:33:29 +02:00
|
|
|
"SetupIntent.create",
|
|
|
|
"SetupIntent.list",
|
|
|
|
"SetupIntent.retrieve",
|
2021-02-12 08:19:30 +01:00
|
|
|
"Subscription.create",
|
|
|
|
"Subscription.delete",
|
|
|
|
"Subscription.retrieve",
|
|
|
|
"Subscription.save",
|
|
|
|
"Token.create",
|
|
|
|
]
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
def mock_stripe(
|
2022-04-27 04:23:43 +02:00
|
|
|
tested_timestamp_fields: Sequence[str] = [], generate: bool = settings.GENERATE_STRIPE_FIXTURES
|
2022-04-13 16:42:42 +02:00
|
|
|
) -> Callable[[Callable[ParamT, ReturnT]], Callable[ParamT, ReturnT]]:
|
|
|
|
def _mock_stripe(decorated_function: Callable[ParamT, ReturnT]) -> Callable[ParamT, ReturnT]:
|
2018-11-07 11:28:36 +01:00
|
|
|
generate_fixture = generate
|
2021-08-29 15:33:29 +02:00
|
|
|
if generate_fixture: # nocoverage
|
|
|
|
assert stripe.api_key
|
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)
|
2022-04-13 16:42:42 +02:00
|
|
|
decorated_function = patch(
|
|
|
|
mocked_function_name,
|
|
|
|
side_effect=side_effect,
|
|
|
|
autospec=mocked_function_name.endswith(".refresh"),
|
|
|
|
)(decorated_function)
|
2018-10-17 08:23:13 +02:00
|
|
|
|
|
|
|
@wraps(decorated_function)
|
2022-04-13 16:42:42 +02:00
|
|
|
def wrapped(*args: ParamT.args, **kwargs: ParamT.kwargs) -> ReturnT:
|
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
|
|
|
|
2022-04-13 16:42:42 +02:00
|
|
|
return 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
|
|
|
|
2019-01-27 21:16:02 +01:00
|
|
|
class StripeTestCase(ZulipTestCase):
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2021-08-14 01:01:37 +02:00
|
|
|
def setUp(self) -> None:
|
2019-10-19 20:47:00 +02:00
|
|
|
super().setUp()
|
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(
|
2021-06-28 15:19:34 +02:00
|
|
|
delivery_email__in=active_emails
|
2021-02-14 00:03:40 +01:00
|
|
|
):
|
|
|
|
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
|
|
|
|
2023-11-02 16:34:37 +01:00
|
|
|
# Make hamlet billing admin for testing.
|
|
|
|
hamlet = self.example_user("hamlet")
|
|
|
|
hamlet.is_billing_admin = True
|
|
|
|
hamlet.save(update_fields=["is_billing_admin"])
|
|
|
|
|
2022-06-08 04:52:09 +02:00
|
|
|
def get_signed_seat_count_from_response(self, response: "TestHttpResponse") -> Optional[str]:
|
2021-08-02 23:20:39 +02:00
|
|
|
match = re.search(r"name=\"signed_seat_count\" value=\"(.+)\"", response.content.decode())
|
2018-08-08 16:35:33 +02:00
|
|
|
return match.group(1) if match else None
|
|
|
|
|
2022-06-08 04:52:09 +02:00
|
|
|
def get_salt_from_response(self, response: "TestHttpResponse") -> Optional[str]:
|
2021-08-02 23:20:39 +02:00
|
|
|
match = re.search(r"name=\"salt\" value=\"(\w+)\"", response.content.decode())
|
2018-08-08 16:35:33 +02:00
|
|
|
return match.group(1) if match else None
|
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
def get_test_card_number(
|
|
|
|
self,
|
|
|
|
attaches_to_customer: bool,
|
|
|
|
charge_succeeds: Optional[bool] = None,
|
|
|
|
card_provider: Optional[str] = None,
|
|
|
|
) -> str:
|
|
|
|
if attaches_to_customer:
|
|
|
|
assert charge_succeeds is not None
|
|
|
|
if charge_succeeds:
|
|
|
|
if card_provider == "visa":
|
|
|
|
return "4242424242424242"
|
|
|
|
if card_provider == "mastercard":
|
|
|
|
return "5555555555554444"
|
|
|
|
raise AssertionError("Unreachable code path")
|
|
|
|
else:
|
|
|
|
return "4000000000000341"
|
|
|
|
else:
|
|
|
|
return "4000000000000002"
|
|
|
|
|
|
|
|
def assert_details_of_valid_session_from_event_status_endpoint(
|
|
|
|
self, stripe_session_id: str, expected_details: Dict[str, Any]
|
|
|
|
) -> None:
|
|
|
|
json_response = self.client_get(
|
|
|
|
"/json/billing/event/status",
|
|
|
|
{
|
|
|
|
"stripe_session_id": stripe_session_id,
|
|
|
|
},
|
|
|
|
)
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(json_response)
|
|
|
|
self.assertEqual(response_dict["session"], expected_details)
|
2021-08-29 15:33:29 +02:00
|
|
|
|
|
|
|
def assert_details_of_valid_payment_intent_from_event_status_endpoint(
|
|
|
|
self,
|
|
|
|
stripe_payment_intent_id: str,
|
|
|
|
expected_details: Dict[str, Any],
|
|
|
|
) -> None:
|
|
|
|
json_response = self.client_get(
|
|
|
|
"/json/billing/event/status",
|
|
|
|
{
|
|
|
|
"stripe_payment_intent_id": stripe_payment_intent_id,
|
|
|
|
},
|
|
|
|
)
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(json_response)
|
|
|
|
self.assertEqual(response_dict["payment_intent"], expected_details)
|
2021-08-29 15:33:29 +02:00
|
|
|
|
|
|
|
def trigger_stripe_checkout_session_completed_webhook(
|
|
|
|
self,
|
|
|
|
payment_method: stripe.PaymentMethod,
|
|
|
|
stripe_session: Optional[stripe.checkout.Session] = None,
|
|
|
|
) -> None:
|
|
|
|
[checkout_setup_intent] = stripe.SetupIntent.list(limit=1)
|
|
|
|
stripe_setup_intent = stripe.SetupIntent.create(
|
|
|
|
payment_method=payment_method.id,
|
|
|
|
confirm=True,
|
|
|
|
payment_method_types=checkout_setup_intent.payment_method_types,
|
|
|
|
customer=checkout_setup_intent.customer,
|
|
|
|
metadata=checkout_setup_intent.metadata,
|
|
|
|
usage=checkout_setup_intent.usage,
|
|
|
|
)
|
|
|
|
if stripe_session is None:
|
|
|
|
[stripe_session] = stripe.checkout.Session.list(limit=1)
|
|
|
|
stripe_session_dict = stripe_session.to_dict_recursive()
|
|
|
|
stripe_session_dict["setup_intent"] = stripe_setup_intent.id
|
|
|
|
|
|
|
|
event_payload = {
|
2022-01-22 07:58:02 +01:00
|
|
|
"id": f"evt_{get_random_string(24)}",
|
2021-08-29 15:33:29 +02:00
|
|
|
"object": "event",
|
|
|
|
"data": {"object": stripe_session_dict},
|
|
|
|
"type": "checkout.session.completed",
|
2021-09-07 17:53:27 +02:00
|
|
|
"api_version": STRIPE_API_VERSION,
|
2021-08-29 15:33:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
response = self.client_post(
|
|
|
|
"/stripe/webhook/", event_payload, content_type="application/json"
|
|
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
def send_stripe_webhook_event(self, event: stripe.Event) -> None:
|
|
|
|
response = self.client_post(
|
|
|
|
"/stripe/webhook/", event.to_dict_recursive(), content_type="application/json"
|
|
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
|
|
|
def send_stripe_webhook_events(self, most_recent_event: stripe.Event) -> None:
|
|
|
|
while True:
|
|
|
|
events_old_to_new = list(reversed(stripe.Event.list(ending_before=most_recent_event)))
|
|
|
|
if len(events_old_to_new) == 0:
|
|
|
|
break
|
|
|
|
for event in events_old_to_new:
|
|
|
|
self.send_stripe_webhook_event(event)
|
|
|
|
most_recent_event = events_old_to_new[-1]
|
|
|
|
|
|
|
|
def send_last_stripe_webhook_event(self) -> None:
|
|
|
|
[last_event] = stripe.Event.list(limit=1)
|
|
|
|
self.send_stripe_webhook_event(last_event)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
def upgrade(
|
|
|
|
self,
|
|
|
|
invoice: bool = False,
|
|
|
|
talk_to_stripe: bool = True,
|
2021-08-29 15:33:29 +02:00
|
|
|
onboarding: bool = False,
|
|
|
|
payment_method: Optional[stripe.PaymentMethod] = None,
|
2022-06-08 04:52:09 +02:00
|
|
|
upgrade_page_response: Optional["TestHttpResponse"] = None,
|
2021-02-12 08:19:30 +01:00
|
|
|
del_args: Sequence[str] = [],
|
|
|
|
**kwargs: Any,
|
2022-06-08 04:52:09 +02:00
|
|
|
) -> "TestHttpResponse":
|
2021-08-29 15:33:29 +02:00
|
|
|
if upgrade_page_response is None:
|
2022-06-23 01:44:03 +02:00
|
|
|
upgrade_page_response = self.client_get("/upgrade/", {})
|
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",
|
2021-08-29 15:33:29 +02:00
|
|
|
"signed_seat_count": self.get_signed_seat_count_from_response(upgrade_page_response),
|
|
|
|
"salt": self.get_salt_from_response(upgrade_page_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
|
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",
|
2021-08-29 15:33:29 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
if onboarding:
|
|
|
|
params.update(
|
|
|
|
onboarding="true",
|
2020-09-03 05:32:15 +02:00
|
|
|
)
|
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]
|
2021-08-29 15:33:29 +02:00
|
|
|
|
|
|
|
if talk_to_stripe:
|
|
|
|
[last_event] = stripe.Event.list(limit=1)
|
|
|
|
|
2022-06-23 01:44:03 +02:00
|
|
|
upgrade_json_response = self.client_post("/json/billing/upgrade", params)
|
2021-08-29 15:33:29 +02:00
|
|
|
|
|
|
|
if invoice or not talk_to_stripe:
|
|
|
|
return upgrade_json_response
|
|
|
|
|
|
|
|
expected_session_details = {"status": "created"}
|
|
|
|
if is_free_trial_offer_enabled():
|
|
|
|
if onboarding:
|
|
|
|
expected_session_details["type"] = "free_trial_upgrade_from_onboarding_page"
|
|
|
|
else:
|
|
|
|
expected_session_details["type"] = "free_trial_upgrade_from_billing_page"
|
|
|
|
else:
|
2022-05-31 01:34:34 +02:00
|
|
|
last_stripe_payment_intent = PaymentIntent.objects.last()
|
|
|
|
assert last_stripe_payment_intent is not None
|
2021-08-29 15:33:29 +02:00
|
|
|
expected_session_details["type"] = "upgrade_from_billing_page"
|
|
|
|
expected_session_details[
|
|
|
|
"stripe_payment_intent_id"
|
2022-05-31 01:34:34 +02:00
|
|
|
] = last_stripe_payment_intent.stripe_payment_intent_id
|
2021-08-29 15:33:29 +02:00
|
|
|
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(upgrade_json_response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"], expected_session_details
|
2021-08-29 15:33:29 +02:00
|
|
|
)
|
|
|
|
if payment_method is None:
|
|
|
|
payment_method = create_payment_method(
|
|
|
|
self.get_test_card_number(
|
|
|
|
attaches_to_customer=True, charge_succeeds=True, card_provider="visa"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.trigger_stripe_checkout_session_completed_webhook(payment_method)
|
|
|
|
self.send_stripe_webhook_events(last_event)
|
|
|
|
return upgrade_json_response
|
2018-11-29 03:15:27 +01:00
|
|
|
|
2019-01-27 21:16:02 +01:00
|
|
|
# Upgrade without talking to Stripe
|
2021-08-29 15:33:29 +02:00
|
|
|
def local_upgrade(
|
|
|
|
self,
|
|
|
|
licenses: int,
|
|
|
|
automanage_licenses: bool,
|
|
|
|
billing_schedule: int,
|
|
|
|
charge_automatically: bool,
|
|
|
|
free_trial: bool,
|
|
|
|
) -> None:
|
2020-06-23 00:31:30 +02:00
|
|
|
class StripeMock(Mock):
|
2022-11-16 06:10:54 +01:00
|
|
|
def __init__(self, depth: int = 1) -> None:
|
2020-06-23 00:31:30 +02:00
|
|
|
super().__init__(spec=stripe.Card)
|
2023-10-30 22:29:22 +01:00
|
|
|
self.id = "cus_123"
|
2021-02-12 08:20:45 +01:00
|
|
|
self.created = "1000"
|
|
|
|
self.last4 = "4242"
|
2019-01-27 21:16:02 +01:00
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
def upgrade_func(
|
|
|
|
licenses: int,
|
|
|
|
automanage_licenses: bool,
|
|
|
|
billing_schedule: int,
|
|
|
|
charge_automatically: bool,
|
|
|
|
free_trial: bool,
|
|
|
|
*mock_args: Any,
|
|
|
|
) -> Any:
|
|
|
|
return process_initial_upgrade(
|
|
|
|
self.example_user("hamlet"),
|
|
|
|
licenses,
|
|
|
|
automanage_licenses,
|
|
|
|
billing_schedule,
|
|
|
|
charge_automatically,
|
|
|
|
free_trial,
|
|
|
|
)
|
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)
|
2021-08-29 15:33:29 +02:00
|
|
|
upgrade_func(
|
|
|
|
licenses, automanage_licenses, billing_schedule, charge_automatically, free_trial
|
|
|
|
)
|
2019-01-27 21:16:02 +01:00
|
|
|
|
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:
|
2021-07-26 19:33:26 +02:00
|
|
|
with self.assertRaises(BillingError) as billing_context:
|
2020-12-23 21:45:16 +01:00
|
|
|
raise_invalid_request_error()
|
2021-07-26 19:33:26 +02:00
|
|
|
self.assertEqual("other stripe error", billing_context.exception.error_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:
|
2021-07-26 19:33:26 +02:00
|
|
|
with self.assertRaises(StripeCardError) as card_context:
|
2020-12-23 21:45:16 +01:00
|
|
|
raise_card_error()
|
2021-07-26 19:33:26 +02:00
|
|
|
self.assertIn("not a valid credit card", str(card_context.exception))
|
|
|
|
self.assertEqual("card error", card_context.exception.error_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)
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertNotEqual(user.realm.plan_type, Realm.PLAN_TYPE_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):
|
2021-08-29 15:33:29 +02:00
|
|
|
response = self.upgrade()
|
|
|
|
[payment_intent] = PaymentIntent.objects.all()
|
|
|
|
assert payment_intent.stripe_payment_intent_id is not None
|
2022-06-07 01:37:01 +02:00
|
|
|
|
|
|
|
response_dict = self.assert_json_success(response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "upgrade_from_billing_page",
|
|
|
|
"status": "completed",
|
|
|
|
"stripe_payment_intent_id": payment_intent.stripe_payment_intent_id,
|
|
|
|
"event_handler": {
|
|
|
|
"status": "succeeded",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
self.assert_details_of_valid_payment_intent_from_event_status_endpoint(
|
|
|
|
payment_intent.stripe_payment_intent_id,
|
|
|
|
{"status": "succeeded", "event_handler": {"status": "succeeded"}},
|
|
|
|
)
|
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(
|
2021-07-25 16:31:12 +02:00
|
|
|
assert_is_not_none(Customer.objects.get(realm=user.realm).stripe_customer_id)
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assertTrue(stripe_customer_has_credit_card_as_default_payment_method(stripe_customer))
|
2018-10-17 08:23:13 +02:00
|
|
|
self.assertEqual(stripe_customer.description, "zulip (Zulip Dev)")
|
|
|
|
self.assertEqual(stripe_customer.discount, None)
|
2021-06-28 15:19:34 +02:00
|
|
|
self.assertEqual(stripe_customer.email, user.delivery_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(
|
2022-02-05 08:29:54 +01:00
|
|
|
charge.description, f"Upgrade to Zulip Cloud Standard, $80.0 x {self.seat_count}"
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2021-06-28 15:19:34 +02:00
|
|
|
self.assertEqual(charge.receipt_email, user.delivery_email)
|
2022-02-05 08:29:54 +01:00
|
|
|
self.assertEqual(charge.statement_descriptor, "Zulip Cloud 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,
|
2021-07-20 13:28:54 +02:00
|
|
|
"collection_method": "charge_automatically",
|
2021-02-12 08:20:45 +01:00
|
|
|
"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,
|
2022-02-05 08:29:54 +01:00
|
|
|
"description": "Zulip Cloud Standard",
|
2021-02-12 08:20:45 +01:00
|
|
|
"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),
|
|
|
|
),
|
2021-08-29 15:33:29 +02:00
|
|
|
(RealmAuditLog.STRIPE_CARD_CHANGED, self.now),
|
2021-02-12 08:19:30 +01:00
|
|
|
(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)
|
2023-07-13 19:46:06 +02:00
|
|
|
first_audit_log_entry = (
|
|
|
|
RealmAuditLog.objects.filter(event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED)
|
|
|
|
.values_list("extra_data", flat=True)
|
|
|
|
.first()
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2023-07-13 19:46:06 +02:00
|
|
|
assert first_audit_log_entry is not None
|
|
|
|
self.assertTrue(first_audit_log_entry["automanage_licenses"])
|
2018-06-28 00:48:51 +02:00
|
|
|
# Check that we correctly updated Realm
|
|
|
|
realm = get_realm("zulip")
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD)
|
2018-10-24 06:09:01 +02:00
|
|
|
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)
|
2022-05-29 21:12:13 +02:00
|
|
|
self.assertEqual("/billing/", response["Location"])
|
2018-03-31 04:13:44 +02:00
|
|
|
|
2023-03-23 04:28:36 +01:00
|
|
|
# Check /billing/ has the correct information
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.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 [
|
2022-02-05 08:29:54 +01:00
|
|
|
"Zulip Cloud 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-06-28 15:19:34 +02:00
|
|
|
f"Billing email: <strong>{user.delivery_email}</strong>",
|
2021-08-29 15:33:29 +02:00
|
|
|
"visa ending in 4242",
|
2021-02-12 08:20:45 +01:00
|
|
|
"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(
|
2021-07-25 16:31:12 +02:00
|
|
|
assert_is_not_none(Customer.objects.get(realm=user.realm).stripe_customer_id)
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assertFalse(stripe_customer_has_credit_card_as_default_payment_method(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,
|
2021-07-20 13:28:54 +02:00
|
|
|
"collection_method": "send_invoice",
|
2022-02-05 08:29:54 +01:00
|
|
|
"statement_descriptor": "Zulip Cloud Standard",
|
2021-02-12 08:20:45 +01:00
|
|
|
"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,
|
2022-02-05 08:29:54 +01:00
|
|
|
"description": "Zulip Cloud Standard",
|
2021-02-12 08:20:45 +01:00
|
|
|
"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)
|
2023-07-13 19:46:06 +02:00
|
|
|
first_audit_log_entry = (
|
|
|
|
RealmAuditLog.objects.filter(event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED)
|
|
|
|
.values_list("extra_data", flat=True)
|
|
|
|
.first()
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2023-07-13 19:46:06 +02:00
|
|
|
assert first_audit_log_entry is not None
|
|
|
|
self.assertFalse(first_audit_log_entry["automanage_licenses"])
|
2018-12-15 09:33:25 +01:00
|
|
|
# Check that we correctly updated Realm
|
|
|
|
realm = get_realm("zulip")
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD)
|
2018-12-15 09:33:25 +01:00
|
|
|
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)
|
2022-05-29 21:12:13 +02:00
|
|
|
self.assertEqual("/billing/", response["Location"])
|
2018-12-15 09:33:25 +01:00
|
|
|
|
2023-03-23 04:28:36 +01:00
|
|
|
# Check /billing/ has the correct information
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.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 [
|
2022-02-05 08:29:54 +01:00
|
|
|
"Zulip Cloud 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-06-28 15:19:34 +02:00
|
|
|
f"Billing email: <strong>{user.delivery_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)
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertNotEqual(user.realm.plan_type, Realm.PLAN_TYPE_STANDARD)
|
2020-04-23 20:10:15 +02:00
|
|
|
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):
|
2021-08-29 15:33:29 +02:00
|
|
|
response = self.upgrade()
|
|
|
|
self.assertEqual(PaymentIntent.objects.count(), 0)
|
2022-06-07 01:37:01 +02:00
|
|
|
|
|
|
|
response_dict = self.assert_json_success(response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "free_trial_upgrade_from_billing_page",
|
|
|
|
"status": "completed",
|
|
|
|
"event_handler": {"status": "succeeded"},
|
|
|
|
},
|
|
|
|
)
|
2020-04-23 20:10:15 +02:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
stripe_customer = stripe_get_customer(
|
2021-07-25 16:31:12 +02:00
|
|
|
assert_is_not_none(Customer.objects.get(realm=user.realm).stripe_customer_id)
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assertTrue(
|
|
|
|
stripe_customer_has_credit_card_as_default_payment_method(stripe_customer)
|
|
|
|
)
|
2020-04-23 20:10:15 +02:00
|
|
|
self.assertEqual(stripe_customer.description, "zulip (Zulip Dev)")
|
|
|
|
self.assertEqual(stripe_customer.discount, None)
|
2021-06-28 15:19:34 +02:00
|
|
|
self.assertEqual(stripe_customer.email, user.delivery_email)
|
2020-04-23 20:10:15 +02:00
|
|
|
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,
|
2021-08-29 15:33:29 +02:00
|
|
|
self.now,
|
2021-02-12 08:19:30 +01:00
|
|
|
),
|
|
|
|
(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)
|
2023-07-13 19:46:06 +02:00
|
|
|
first_audit_log_entry = (
|
|
|
|
RealmAuditLog.objects.filter(event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED)
|
|
|
|
.values_list("extra_data", flat=True)
|
|
|
|
.first()
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2023-07-13 19:46:06 +02:00
|
|
|
assert first_audit_log_entry is not None
|
|
|
|
self.assertTrue(first_audit_log_entry["automanage_licenses"])
|
2020-04-23 20:10:15 +02:00
|
|
|
|
|
|
|
realm = get_realm("zulip")
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD)
|
2020-04-23 20:10:15 +02:00
|
|
|
self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
|
|
|
|
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.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 [
|
2022-02-05 08:29:54 +01:00
|
|
|
"Zulip Cloud Standard",
|
2021-02-12 08:20:45 +01:00
|
|
|
"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-06-28 15:19:34 +02:00
|
|
|
f"Billing email: <strong>{user.delivery_email}</strong>",
|
2021-08-29 15:33:29 +02:00
|
|
|
"visa ending in 4242",
|
2021-02-12 08:20:45 +01:00
|
|
|
"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-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.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))
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_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,
|
|
|
|
"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,
|
2022-02-05 08:29:54 +01:00
|
|
|
"description": "Zulip Cloud Standard - renewal",
|
2021-02-12 08:19:30 +01:00
|
|
|
"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,
|
|
|
|
"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
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
@mock_stripe(tested_timestamp_fields=["created"])
|
|
|
|
def test_free_trial_upgrade_by_card_from_onboarding_page(self, *mocks: Mock) -> None:
|
|
|
|
user = self.example_user("hamlet")
|
|
|
|
self.login_user(user)
|
|
|
|
|
|
|
|
with self.settings(FREE_TRIAL_DAYS=60):
|
|
|
|
free_trial_end_date = self.now + timedelta(days=60)
|
|
|
|
self.assertNotEqual(user.realm.plan_type, Realm.PLAN_TYPE_STANDARD)
|
|
|
|
self.assertFalse(Customer.objects.filter(realm=user.realm).exists())
|
|
|
|
|
|
|
|
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
|
|
|
response = self.upgrade(onboarding=True)
|
|
|
|
self.assertEqual(PaymentIntent.objects.all().count(), 0)
|
2022-06-07 01:37:01 +02:00
|
|
|
|
|
|
|
response_dict = self.assert_json_success(response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "free_trial_upgrade_from_onboarding_page",
|
|
|
|
"status": "completed",
|
|
|
|
"event_handler": {"status": "succeeded"},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
stripe_customer = stripe_get_customer(
|
|
|
|
assert_is_not_none(Customer.objects.get(realm=user.realm).stripe_customer_id)
|
|
|
|
)
|
|
|
|
self.assertTrue(
|
|
|
|
stripe_customer_has_credit_card_as_default_payment_method(stripe_customer)
|
|
|
|
)
|
|
|
|
self.assertEqual(stripe_customer.description, "zulip (Zulip Dev)")
|
|
|
|
self.assertEqual(stripe_customer.discount, None)
|
|
|
|
self.assertEqual(stripe_customer.email, user.delivery_email)
|
|
|
|
metadata_dict = dict(stripe_customer.metadata)
|
|
|
|
self.assertEqual(metadata_dict["realm_str"], "zulip")
|
|
|
|
try:
|
|
|
|
int(metadata_dict["realm_id"])
|
|
|
|
except ValueError: # nocoverage
|
|
|
|
raise AssertionError("realm_id is not a number")
|
|
|
|
self.assertFalse(stripe.Charge.list(customer=stripe_customer.id))
|
|
|
|
self.assertFalse(stripe.Invoice.list(customer=stripe_customer.id))
|
|
|
|
customer = Customer.objects.get(stripe_customer_id=stripe_customer.id, realm=user.realm)
|
|
|
|
customer = Customer.objects.get(stripe_customer_id=stripe_customer.id, realm=user.realm)
|
|
|
|
plan = CustomerPlan.objects.get(
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
LicenseLedger.objects.get(
|
|
|
|
plan=plan,
|
|
|
|
is_renewal=True,
|
|
|
|
event_time=self.now,
|
|
|
|
licenses=self.seat_count,
|
|
|
|
licenses_at_next_renewal=self.seat_count,
|
|
|
|
)
|
|
|
|
# We don't test anything else since test_free_trial_upgrade_by_card does this already.
|
|
|
|
|
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)
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertNotEqual(user.realm.plan_type, Realm.PLAN_TYPE_STANDARD)
|
2020-04-23 20:10:15 +02:00
|
|
|
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(
|
2021-07-25 16:31:12 +02:00
|
|
|
assert_is_not_none(Customer.objects.get(realm=user.realm).stripe_customer_id)
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2020-04-23 20:10:15 +02:00
|
|
|
self.assertEqual(stripe_customer.discount, None)
|
2021-06-28 15:19:34 +02:00
|
|
|
self.assertEqual(stripe_customer.email, user.delivery_email)
|
2020-04-23 20:10:15 +02:00
|
|
|
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)
|
2023-07-13 19:46:06 +02:00
|
|
|
first_audit_log_entry = (
|
|
|
|
RealmAuditLog.objects.filter(event_type=RealmAuditLog.CUSTOMER_PLAN_CREATED)
|
|
|
|
.values_list("extra_data", flat=True)
|
|
|
|
.first()
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2023-07-13 19:46:06 +02:00
|
|
|
assert first_audit_log_entry is not None
|
|
|
|
self.assertFalse(first_audit_log_entry["automanage_licenses"])
|
2020-04-23 20:10:15 +02:00
|
|
|
|
|
|
|
realm = get_realm("zulip")
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD)
|
2020-04-23 20:10:15 +02:00
|
|
|
self.assertEqual(realm.max_invites, Realm.INVITES_STANDARD_REALM_DAILY_MAX)
|
|
|
|
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.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 [
|
2022-02-05 08:29:54 +01:00
|
|
|
"Zulip Cloud Standard",
|
2021-02-12 08:20:45 +01:00
|
|
|
"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-06-28 15:19:34 +02:00
|
|
|
f"Billing email: <strong>{user.delivery_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))
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_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,
|
|
|
|
"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,
|
2022-02-05 08:29:54 +01:00
|
|
|
"description": "Zulip Cloud Standard - renewal",
|
2021-02-12 08:19:30 +01:00
|
|
|
"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-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):
|
2021-08-29 15:33:29 +02:00
|
|
|
with patch(
|
|
|
|
"corporate.views.upgrade.get_latest_seat_count", return_value=self.seat_count
|
|
|
|
):
|
|
|
|
self.upgrade()
|
2021-07-24 16:56:39 +02:00
|
|
|
customer = Customer.objects.first()
|
|
|
|
assert customer is not None
|
2021-07-25 16:31:12 +02:00
|
|
|
stripe_customer_id: str = assert_is_not_none(customer.stripe_customer_id)
|
2018-12-15 09:33:25 +01:00
|
|
|
# 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
|
2021-07-24 16:56:39 +02:00
|
|
|
ledger_entry = LicenseLedger.objects.first()
|
|
|
|
assert ledger_entry is not None
|
|
|
|
self.assertEqual(ledger_entry.licenses, new_seat_count)
|
|
|
|
self.assertEqual(ledger_entry.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()
|
2023-01-02 20:50:23 +01:00
|
|
|
def test_upgrade_first_card_fails_and_retry_with_another_card_without_starting_from_beginning(
|
2021-08-29 15:33:29 +02:00
|
|
|
self, *mocks: Mock
|
|
|
|
) -> None:
|
|
|
|
user = self.example_user("hamlet")
|
|
|
|
self.login_user(user)
|
|
|
|
# From https://stripe.com/docs/testing#cards: Attaching this card to
|
|
|
|
# a Customer object succeeds, but attempts to charge the customer fail.
|
|
|
|
with self.assertLogs("corporate.stripe", "INFO") as m:
|
|
|
|
response = self.upgrade(
|
|
|
|
payment_method=create_payment_method(
|
|
|
|
self.get_test_card_number(attaches_to_customer=True, charge_succeeds=False)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
m.output,
|
|
|
|
[
|
|
|
|
"INFO:corporate.stripe:Stripe payment intent failed: zulip card_error card_declined None"
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
|
|
|
[payment_intent] = PaymentIntent.objects.all()
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "upgrade_from_billing_page",
|
|
|
|
"status": "completed",
|
|
|
|
"stripe_payment_intent_id": payment_intent.stripe_payment_intent_id,
|
|
|
|
"event_handler": {
|
|
|
|
"status": "succeeded",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
self.assert_details_of_valid_payment_intent_from_event_status_endpoint(
|
|
|
|
payment_intent.stripe_payment_intent_id,
|
|
|
|
{
|
|
|
|
"status": "requires_payment_method",
|
|
|
|
"last_payment_error": {
|
|
|
|
"message": "Your card was declined.",
|
|
|
|
"description": "card_error",
|
|
|
|
},
|
|
|
|
"event_handler": {"status": "succeeded"},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
# Check that we created a Customer object but no CustomerPlan
|
|
|
|
stripe_customer_id = Customer.objects.get(realm=get_realm("zulip")).stripe_customer_id
|
|
|
|
assert stripe_customer_id is not None
|
|
|
|
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))
|
|
|
|
[charge] = stripe.Charge.list(customer=stripe_customer_id)
|
|
|
|
self.assertEqual(charge.failure_code, "card_declined")
|
|
|
|
self.assertFalse(stripe.Invoice.list(customer=stripe_customer_id))
|
|
|
|
self.assertFalse(stripe.InvoiceItem.list(customer=stripe_customer_id))
|
|
|
|
# Check that we correctly populated RealmAuditLog
|
|
|
|
audit_log_entries = list(
|
|
|
|
RealmAuditLog.objects.filter(acting_user=user)
|
|
|
|
.values_list("event_type", flat=True)
|
|
|
|
.order_by("id")
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
audit_log_entries,
|
|
|
|
[RealmAuditLog.STRIPE_CUSTOMER_CREATED, RealmAuditLog.STRIPE_CARD_CHANGED],
|
|
|
|
)
|
|
|
|
# Check that we did not update Realm
|
|
|
|
realm = get_realm("zulip")
|
|
|
|
self.assertNotEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD)
|
|
|
|
# Check that we still get redirected to /upgrade
|
|
|
|
response = self.client_get("/billing/")
|
|
|
|
self.assertEqual(response.status_code, 302)
|
2022-05-29 21:12:13 +02:00
|
|
|
self.assertEqual("/upgrade/", response["Location"])
|
2021-08-29 15:33:29 +02:00
|
|
|
|
|
|
|
[last_event] = stripe.Event.list(limit=1)
|
|
|
|
retry_payment_intent_json_response = self.client_post(
|
|
|
|
"/json/billing/session/start_retry_payment_intent_session",
|
|
|
|
{
|
|
|
|
"stripe_payment_intent_id": payment_intent.stripe_payment_intent_id,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
self.assert_json_success(retry_payment_intent_json_response)
|
|
|
|
[payment_intent] = PaymentIntent.objects.all()
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(retry_payment_intent_json_response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "retry_upgrade_with_another_payment_method",
|
|
|
|
"status": "created",
|
|
|
|
"stripe_payment_intent_id": payment_intent.stripe_payment_intent_id,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
self.trigger_stripe_checkout_session_completed_webhook(
|
|
|
|
create_payment_method(
|
|
|
|
self.get_test_card_number(
|
|
|
|
attaches_to_customer=True, charge_succeeds=True, card_provider="visa"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.assert_details_of_valid_payment_intent_from_event_status_endpoint(
|
|
|
|
payment_intent.stripe_payment_intent_id,
|
|
|
|
{"status": "processing"},
|
|
|
|
)
|
|
|
|
self.send_stripe_webhook_events(last_event)
|
|
|
|
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(retry_payment_intent_json_response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "retry_upgrade_with_another_payment_method",
|
|
|
|
"status": "completed",
|
|
|
|
"stripe_payment_intent_id": payment_intent.stripe_payment_intent_id,
|
|
|
|
"event_handler": {
|
|
|
|
"status": "succeeded",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
self.assert_details_of_valid_payment_intent_from_event_status_endpoint(
|
|
|
|
payment_intent.stripe_payment_intent_id,
|
|
|
|
{"status": "succeeded", "event_handler": {"status": "succeeded"}},
|
|
|
|
)
|
|
|
|
|
|
|
|
retry_payment_intent_json_response = self.client_post(
|
|
|
|
"/json/billing/session/start_retry_payment_intent_session",
|
|
|
|
{
|
|
|
|
"stripe_payment_intent_id": payment_intent.stripe_payment_intent_id,
|
|
|
|
},
|
|
|
|
)
|
|
|
|
self.assert_json_error(retry_payment_intent_json_response, "Payment already succeeded.")
|
|
|
|
|
|
|
|
customer = Customer.objects.get(realm=get_realm("zulip"))
|
|
|
|
# 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)
|
|
|
|
# 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, self.seat_count)
|
|
|
|
self.assertEqual(ledger_entry.licenses_at_next_renewal, self.seat_count)
|
|
|
|
# Check the Charges and Invoices in Stripe
|
|
|
|
[charge0, charge1] = stripe.Charge.list(customer=stripe_customer_id)
|
|
|
|
self.assertEqual(8000 * self.seat_count, charge0.amount)
|
|
|
|
[stripe_invoice] = stripe.Invoice.list(customer=stripe_customer_id)
|
|
|
|
self.assertEqual(
|
|
|
|
[8000 * self.seat_count, -8000 * self.seat_count],
|
|
|
|
[item.amount for item in stripe_invoice.lines],
|
|
|
|
)
|
|
|
|
# Check that we correctly populated RealmAuditLog
|
|
|
|
audit_log_entries = list(
|
|
|
|
RealmAuditLog.objects.filter(acting_user=user)
|
|
|
|
.values_list("event_type", flat=True)
|
|
|
|
.order_by("id")
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
audit_log_entries,
|
|
|
|
[
|
|
|
|
RealmAuditLog.STRIPE_CUSTOMER_CREATED,
|
|
|
|
RealmAuditLog.STRIPE_CARD_CHANGED,
|
|
|
|
RealmAuditLog.STRIPE_CARD_CHANGED,
|
|
|
|
RealmAuditLog.CUSTOMER_PLAN_CREATED,
|
|
|
|
RealmAuditLog.REALM_PLAN_TYPE_CHANGED,
|
|
|
|
],
|
|
|
|
)
|
|
|
|
# Check that we correctly updated Realm
|
|
|
|
realm = get_realm("zulip")
|
|
|
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD)
|
|
|
|
# Check that we can no longer access /upgrade
|
|
|
|
response = self.client_get("/upgrade/")
|
|
|
|
self.assertEqual(response.status_code, 302)
|
2022-05-29 21:12:13 +02:00
|
|
|
self.assertEqual("/billing/", response["Location"])
|
2021-08-29 15:33:29 +02:00
|
|
|
|
|
|
|
@mock_stripe()
|
2023-01-02 20:50:23 +01:00
|
|
|
def test_upgrade_first_card_fails_and_restart_from_beginning(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:
|
2021-08-29 15:33:29 +02:00
|
|
|
response = self.upgrade(
|
|
|
|
payment_method=create_payment_method(
|
|
|
|
self.get_test_card_number(attaches_to_customer=True, charge_succeeds=False)
|
|
|
|
)
|
|
|
|
)
|
2021-02-12 08:19:30 +01:00
|
|
|
self.assertEqual(
|
|
|
|
m.output,
|
2021-08-29 15:33:29 +02:00
|
|
|
[
|
|
|
|
"INFO:corporate.stripe:Stripe payment intent failed: zulip card_error card_declined None"
|
|
|
|
],
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2021-08-29 15:33:29 +02:00
|
|
|
|
|
|
|
[payment_intent] = PaymentIntent.objects.all()
|
|
|
|
assert payment_intent.stripe_payment_intent_id is not None
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "upgrade_from_billing_page",
|
|
|
|
"status": "completed",
|
|
|
|
"stripe_payment_intent_id": payment_intent.stripe_payment_intent_id,
|
|
|
|
"event_handler": {
|
|
|
|
"status": "succeeded",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
self.assert_details_of_valid_payment_intent_from_event_status_endpoint(
|
|
|
|
payment_intent.stripe_payment_intent_id,
|
|
|
|
{
|
|
|
|
"status": "requires_payment_method",
|
|
|
|
"last_payment_error": {
|
|
|
|
"message": "Your card was declined.",
|
|
|
|
"description": "card_error",
|
|
|
|
},
|
|
|
|
"event_handler": {"status": "succeeded"},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
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
|
2021-07-24 16:56:39 +02:00
|
|
|
assert stripe_customer_id is not None
|
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")
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertNotEqual(realm.plan_type, Realm.PLAN_TYPE_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)
|
2022-05-29 21:12:13 +02:00
|
|
|
self.assertEqual("/upgrade/", response["Location"])
|
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):
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.upgrade.get_latest_seat_count", return_value=23):
|
2021-08-29 15:33:29 +02:00
|
|
|
response = self.upgrade()
|
|
|
|
[second_payment_intent, _] = PaymentIntent.objects.all().order_by("-id")
|
|
|
|
assert second_payment_intent.stripe_payment_intent_id is not None
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "upgrade_from_billing_page",
|
|
|
|
"status": "completed",
|
|
|
|
"stripe_payment_intent_id": second_payment_intent.stripe_payment_intent_id,
|
|
|
|
"event_handler": {
|
|
|
|
"status": "succeeded",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
self.assert_details_of_valid_payment_intent_from_event_status_endpoint(
|
|
|
|
second_payment_intent.stripe_payment_intent_id,
|
|
|
|
{"status": "succeeded", "event_handler": {"status": "succeeded"}},
|
|
|
|
)
|
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")
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_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)
|
2022-05-29 21:12:13 +02:00
|
|
|
self.assertEqual("/billing/", response["Location"])
|
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-08-29 15:33:29 +02:00
|
|
|
with self.assertLogs("corporate.stripe", "WARNING"):
|
|
|
|
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
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
@mock_stripe()
|
|
|
|
def test_upgrade_race_condition_during_card_upgrade(self, *mocks: Mock) -> None:
|
|
|
|
hamlet = self.example_user("hamlet")
|
|
|
|
othello = self.example_user("othello")
|
|
|
|
|
|
|
|
self.login_user(hamlet)
|
|
|
|
hamlet_upgrade_page_response = self.client_get("/upgrade/")
|
|
|
|
self.client_post(
|
|
|
|
"/json/billing/upgrade",
|
|
|
|
{
|
|
|
|
"billing_modality": "charge_automatically",
|
|
|
|
"schedule": "annual",
|
|
|
|
"signed_seat_count": self.get_signed_seat_count_from_response(
|
|
|
|
hamlet_upgrade_page_response
|
|
|
|
),
|
|
|
|
"salt": self.get_salt_from_response(hamlet_upgrade_page_response),
|
|
|
|
"license_management": "automatic",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
[hamlet_stripe_session] = stripe.checkout.Session.list(limit=1)
|
|
|
|
[hamlet_payment_intent] = stripe.PaymentIntent.list(limit=1)
|
|
|
|
|
|
|
|
self.login_user(othello)
|
|
|
|
self.upgrade()
|
|
|
|
|
|
|
|
self.login_user(hamlet)
|
|
|
|
# Checkout session cannot be started since the organization has been already upgraded.
|
|
|
|
with self.assertLogs("corporate.stripe", "WARNING"):
|
|
|
|
response = self.client_post(
|
|
|
|
"/json/billing/upgrade",
|
|
|
|
{
|
|
|
|
"billing_modality": "charge_automatically",
|
|
|
|
"schedule": "annual",
|
|
|
|
"signed_seat_count": self.get_signed_seat_count_from_response(
|
|
|
|
hamlet_upgrade_page_response
|
|
|
|
),
|
|
|
|
"salt": self.get_salt_from_response(hamlet_upgrade_page_response),
|
|
|
|
"license_management": "automatic",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
self.assert_json_error_contains(
|
|
|
|
response,
|
|
|
|
"The organization is already subscribed to a plan. Please reload the billing page.",
|
|
|
|
)
|
|
|
|
payment_method = create_payment_method(
|
|
|
|
self.get_test_card_number(
|
|
|
|
attaches_to_customer=True, charge_succeeds=True, card_provider="visa"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
# Organization has been upgraded by the time hamlet completes the checkout session.
|
|
|
|
with self.assertLogs("corporate.stripe", "WARNING"):
|
|
|
|
self.trigger_stripe_checkout_session_completed_webhook(
|
|
|
|
payment_method, hamlet_stripe_session
|
|
|
|
)
|
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
|
|
|
hamlet_stripe_session.id,
|
|
|
|
{
|
|
|
|
"type": "upgrade_from_billing_page",
|
|
|
|
"status": "completed",
|
|
|
|
"stripe_payment_intent_id": hamlet_payment_intent.id,
|
|
|
|
"event_handler": {
|
|
|
|
"status": "failed",
|
|
|
|
"error": {
|
|
|
|
"message": "The organization is already subscribed to a plan. Please reload the billing page.",
|
|
|
|
"description": "subscribing with existing subscription",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
# Organization has been upgraded by the time payment intent is successful.
|
|
|
|
stripe.PaymentIntent.confirm(
|
|
|
|
hamlet_payment_intent.id,
|
|
|
|
payment_method=payment_method.id,
|
|
|
|
off_session=True,
|
|
|
|
)
|
|
|
|
with self.assertLogs("corporate.stripe", "WARNING"):
|
|
|
|
self.send_last_stripe_webhook_event()
|
|
|
|
self.assert_details_of_valid_payment_intent_from_event_status_endpoint(
|
|
|
|
hamlet_payment_intent.id,
|
|
|
|
{
|
|
|
|
"status": "succeeded",
|
|
|
|
"event_handler": {
|
|
|
|
"status": "failed",
|
|
|
|
"error": {
|
|
|
|
"message": "The organization is already subscribed to a plan. Please reload the billing page.",
|
|
|
|
"description": "subscribing with existing subscription",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
charged_amount = self.seat_count * 8000
|
|
|
|
customer = get_customer_by_realm(get_realm("zulip"))
|
|
|
|
assert customer is not None
|
|
|
|
assert customer.stripe_customer_id is not None
|
|
|
|
[invoice, _] = stripe.Invoice.list(customer=customer.stripe_customer_id)
|
|
|
|
self.assertEqual(invoice.total, -1 * charged_amount)
|
|
|
|
stripe_customer = stripe.Customer.retrieve(customer.stripe_customer_id)
|
|
|
|
self.assertEqual(stripe_customer.balance, -1 * charged_amount)
|
|
|
|
|
|
|
|
def test_upgrade_race_condition_during_invoice_upgrade(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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2021-02-12 08:20:45 +01:00
|
|
|
with self.assertLogs("corporate.stripe", "WARNING") as m:
|
2019-01-29 16:01:31 +01:00
|
|
|
with self.assertRaises(BillingError) as context:
|
2021-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2021-07-04 08:19:18 +02:00
|
|
|
self.assertEqual(
|
|
|
|
"subscribing with existing subscription", context.exception.error_description
|
|
|
|
)
|
2021-02-04 11:20:53 +01:00
|
|
|
self.assertEqual(
|
2021-02-12 08:19:30 +01:00
|
|
|
m.output[0],
|
2023-10-30 22:29:22 +01:00
|
|
|
"WARNING:corporate.stripe:Upgrade of <Realm: zulip 2> (with stripe_customer_id: cus_123) failed because of existing active plan.",
|
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:
|
2021-04-09 12:43:44 +02:00
|
|
|
if error_description:
|
2021-08-29 15:33:29 +02:00
|
|
|
with self.assertLogs("corporate.stripe", "WARNING"):
|
|
|
|
response = self.upgrade(
|
|
|
|
talk_to_stripe=False, del_args=del_args, **upgrade_params
|
|
|
|
)
|
|
|
|
self.assertEqual(
|
|
|
|
orjson.loads(response.content)["error_description"], error_description
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
response = self.upgrade(talk_to_stripe=False, del_args=del_args, **upgrade_params)
|
|
|
|
self.assert_json_error_contains(response, error_message)
|
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"})
|
2021-08-29 15:33:29 +02:00
|
|
|
|
2021-04-09 12:43:44 +02:00
|
|
|
check_error(
|
2021-08-29 15:33:29 +02:00
|
|
|
"You must invoice for at least 30 users.",
|
|
|
|
"not enough licenses",
|
|
|
|
{"billing_modality": "send_invoice", "licenses": -1},
|
|
|
|
)
|
|
|
|
check_error(
|
|
|
|
"You must invoice for at least 30 users.",
|
|
|
|
"not enough licenses",
|
|
|
|
{"billing_modality": "send_invoice"},
|
|
|
|
)
|
|
|
|
check_error(
|
|
|
|
"You must invoice for at least 30 users.",
|
|
|
|
"not enough licenses",
|
|
|
|
{"billing_modality": "send_invoice", "licenses": 25},
|
|
|
|
)
|
|
|
|
check_error(
|
|
|
|
"Invoices with more than 1000 licenses can't be processed from this page",
|
|
|
|
"too many licenses",
|
|
|
|
{"billing_modality": "send_invoice", "licenses": 10000},
|
|
|
|
)
|
|
|
|
|
|
|
|
check_error(
|
|
|
|
"You must invoice for at least 6 users.",
|
|
|
|
"not enough licenses",
|
|
|
|
{"billing_modality": "charge_automatically", "license_management": "manual"},
|
|
|
|
)
|
|
|
|
|
|
|
|
check_error(
|
|
|
|
"You must invoice for at least 6 users.",
|
|
|
|
"not enough licenses",
|
|
|
|
{
|
|
|
|
"billing_modality": "charge_automatically",
|
|
|
|
"license_management": "manual",
|
|
|
|
"licenses": 3,
|
|
|
|
},
|
2021-04-09 12:43:44 +02:00
|
|
|
)
|
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,
|
2022-10-06 11:56:48 +02:00
|
|
|
upgrade_params: Mapping[str, Any] = {},
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> None:
|
2022-10-06 11:56:48 +02:00
|
|
|
upgrade_params = dict(upgrade_params)
|
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-08-29 15:33:29 +02:00
|
|
|
with self.assertLogs("corporate.stripe", "WARNING"):
|
|
|
|
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-08-29 15:33:29 +02:00
|
|
|
with self.assertLogs("corporate.stripe", "WARNING"):
|
|
|
|
response = self.upgrade(invoice=True, talk_to_stripe=False, licenses=licenses)
|
2021-02-12 08:19:30 +01:00
|
|
|
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(
|
2022-10-06 11:56:48 +02:00
|
|
|
invoice: bool, licenses: Optional[int], upgrade_params: Mapping[str, Any] = {}
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> None:
|
2022-10-06 11:56:48 +02:00
|
|
|
upgrade_params = dict(upgrade_params)
|
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-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.upgrade.process_initial_upgrade"):
|
2021-08-29 15:33:29 +02:00
|
|
|
stripe_session = stripe.checkout.Session()
|
|
|
|
stripe_session.id = "stripe_session_id"
|
|
|
|
stripe_session.url = "stripe_session_url"
|
|
|
|
with patch(
|
|
|
|
"corporate.views.upgrade.setup_upgrade_checkout_session_and_payment_intent",
|
|
|
|
return_value=stripe_session,
|
|
|
|
):
|
|
|
|
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
|
|
|
|
2023-04-10 21:48:52 +02:00
|
|
|
# By default, an organization on a "Pay by card" plan with Manual license
|
|
|
|
# management cannot purchase less licenses than the current seat count.
|
|
|
|
# If exempt_from_license_number_check is enabled, they should be able to though.
|
|
|
|
customer = Customer.objects.get_or_create(realm=hamlet.realm)[0]
|
|
|
|
customer.exempt_from_license_number_check = True
|
|
|
|
customer.save()
|
|
|
|
check_success(False, self.seat_count - 1, {"license_management": "manual"})
|
|
|
|
|
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(
|
2021-08-29 15:33:29 +02:00
|
|
|
"corporate.views.upgrade.setup_upgrade_checkout_session_and_payment_intent",
|
|
|
|
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
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
@mock_stripe()
|
|
|
|
def test_checkout_session_completed_with_uncaught_exception(self, *mock_args: Any) -> None:
|
|
|
|
hamlet = self.example_user("hamlet")
|
|
|
|
self.login_user(hamlet)
|
|
|
|
|
|
|
|
with patch(
|
2023-10-26 14:11:43 +02:00
|
|
|
"corporate.lib.stripe_event_handler.RealmBillingSession",
|
2021-08-29 15:33:29 +02:00
|
|
|
side_effect=Exception,
|
|
|
|
), self.assertLogs("corporate.stripe", "WARNING"):
|
|
|
|
response = self.upgrade()
|
|
|
|
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "upgrade_from_billing_page",
|
|
|
|
"status": "completed",
|
|
|
|
"stripe_payment_intent_id": PaymentIntent.objects.get().stripe_payment_intent_id,
|
|
|
|
"event_handler": {
|
|
|
|
"status": "failed",
|
|
|
|
"error": {
|
|
|
|
"message": "Something went wrong. Please contact desdemona+admin@zulip.com.",
|
|
|
|
"description": "uncaught exception in checkout.session.completed event handler",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
@mock_stripe()
|
|
|
|
def test_payment_intent_succeeded_event_with_uncaught_exception(self, *mock_args: Any) -> None:
|
|
|
|
hamlet = self.example_user("hamlet")
|
|
|
|
self.login_user(hamlet)
|
|
|
|
|
|
|
|
with patch(
|
|
|
|
"corporate.lib.stripe_event_handler.process_initial_upgrade", side_effect=Exception
|
|
|
|
), self.assertLogs("corporate.stripe", "WARNING"):
|
|
|
|
response = self.upgrade()
|
|
|
|
|
|
|
|
[payment_intent] = PaymentIntent.objects.all().order_by("-id")
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "upgrade_from_billing_page",
|
|
|
|
"status": "completed",
|
|
|
|
"stripe_payment_intent_id": payment_intent.stripe_payment_intent_id,
|
|
|
|
"event_handler": {
|
|
|
|
"status": "succeeded",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
self.assert_details_of_valid_payment_intent_from_event_status_endpoint(
|
|
|
|
payment_intent.stripe_payment_intent_id,
|
|
|
|
{
|
|
|
|
"status": "succeeded",
|
|
|
|
"event_handler": {
|
|
|
|
"status": "failed",
|
|
|
|
"error": {
|
|
|
|
"message": "Something went wrong. Please contact desdemona+admin@zulip.com.",
|
|
|
|
"description": "uncaught exception in payment_intent.succeeded event handler",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
@mock_stripe()
|
|
|
|
def test_restart_payment_intent_session_errors(self, *mocks: Any) -> None:
|
|
|
|
user = self.example_user("hamlet")
|
|
|
|
self.login_user(user)
|
|
|
|
|
|
|
|
json_response = self.client_post("/json/billing/session/start_retry_payment_intent_session")
|
|
|
|
self.assert_json_error(json_response, "Missing 'stripe_payment_intent_id' argument")
|
|
|
|
|
|
|
|
json_response = self.client_post(
|
|
|
|
"/json/billing/session/start_retry_payment_intent_session",
|
|
|
|
{"stripe_payment_intent_id": "stripe_payment_intent_id"},
|
|
|
|
)
|
|
|
|
self.assert_json_error(json_response, "Please create a customer first.")
|
|
|
|
|
|
|
|
upgrade_page_response = self.client_get("/upgrade/")
|
|
|
|
self.client_post(
|
|
|
|
"/json/billing/upgrade",
|
|
|
|
{
|
|
|
|
"billing_modality": "charge_automatically",
|
|
|
|
"schedule": "monthly",
|
|
|
|
"signed_seat_count": self.get_signed_seat_count_from_response(
|
|
|
|
upgrade_page_response
|
|
|
|
),
|
|
|
|
"salt": self.get_salt_from_response(upgrade_page_response),
|
|
|
|
"license_management": "automatic",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
response = self.client_post(
|
|
|
|
"/json/billing/session/start_retry_payment_intent_session",
|
|
|
|
{"stripe_payment_intent_id": "stripe_payment_intent_id"},
|
|
|
|
)
|
|
|
|
self.assert_json_error(response, "Invalid payment intent id.")
|
|
|
|
|
2021-08-10 20:52:01 +02:00
|
|
|
def test_request_sponsorship_form_with_invalid_url(self) -> None:
|
|
|
|
user = self.example_user("hamlet")
|
|
|
|
self.login_user(user)
|
|
|
|
data = {
|
|
|
|
"organization-type": Realm.ORG_TYPES["opensource"]["id"],
|
|
|
|
"website": "invalid-url",
|
|
|
|
"description": "Infinispan is a distributed in-memory key/value data store with optional schema.",
|
|
|
|
}
|
|
|
|
|
|
|
|
response = self.client_post("/json/billing/sponsorship", data)
|
|
|
|
|
|
|
|
self.assert_json_error(response, "Enter a valid URL.")
|
|
|
|
|
|
|
|
def test_request_sponsorship_form_with_blank_url(self) -> None:
|
|
|
|
user = self.example_user("hamlet")
|
|
|
|
self.login_user(user)
|
|
|
|
data = {
|
|
|
|
"organization-type": Realm.ORG_TYPES["opensource"]["id"],
|
|
|
|
"website": "",
|
|
|
|
"description": "Infinispan is a distributed in-memory key/value data store with optional schema.",
|
|
|
|
}
|
|
|
|
|
|
|
|
response = self.client_post("/json/billing/sponsorship", data)
|
|
|
|
|
|
|
|
self.assert_json_success(response)
|
|
|
|
|
2021-09-29 19:51:55 +02:00
|
|
|
def test_support_request(self) -> None:
|
|
|
|
user = self.example_user("hamlet")
|
|
|
|
self.assertIsNone(get_customer_by_realm(user.realm))
|
|
|
|
|
|
|
|
self.login_user(user)
|
|
|
|
|
|
|
|
result = self.client_get("/support/")
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
self.assert_in_success_response(["Contact support"], result)
|
|
|
|
|
|
|
|
data = {
|
|
|
|
"request_subject": "Not getting messages.",
|
|
|
|
"request_message": "Running into this weird issue.",
|
|
|
|
}
|
|
|
|
result = self.client_post("/support/", data)
|
2021-10-02 02:37:27 +02:00
|
|
|
self.assert_in_success_response(["Thanks for contacting us!"], result)
|
2021-09-29 19:51:55 +02:00
|
|
|
|
|
|
|
from django.core.mail import outbox
|
|
|
|
|
|
|
|
self.assert_length(outbox, 1)
|
|
|
|
|
|
|
|
for message in outbox:
|
|
|
|
self.assert_length(message.to, 1)
|
|
|
|
self.assertEqual(message.to[0], "desdemona+admin@zulip.com")
|
|
|
|
self.assertEqual(message.subject, "Support request for zulip")
|
|
|
|
self.assertEqual(message.reply_to, ["hamlet@zulip.com"])
|
|
|
|
self.assertEqual(self.email_envelope_from(message), settings.NOREPLY_EMAIL_ADDRESS)
|
|
|
|
self.assertIn("Zulip Support <noreply-", self.email_display_from(message))
|
|
|
|
self.assertIn("Requested by: King Hamlet (Member)", message.body)
|
|
|
|
self.assertIn(
|
|
|
|
"Support URL: http://zulip.testserver/activity/support?q=zulip", message.body
|
|
|
|
)
|
|
|
|
self.assertIn("Subject: Not getting messages.", message.body)
|
|
|
|
self.assertIn("Message:\nRunning into this weird issue", message.body)
|
|
|
|
|
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-07-09 19:56:55 +02:00
|
|
|
"organization-type": Realm.ORG_TYPES["opensource"]["id"],
|
2021-04-09 12:43:44 +02:00
|
|
|
"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)
|
|
|
|
|
2021-07-09 19:56:55 +02:00
|
|
|
sponsorship_request = ZulipSponsorshipRequest.objects.filter(
|
|
|
|
realm=user.realm, requested_by=user
|
|
|
|
).first()
|
|
|
|
assert sponsorship_request is not None
|
|
|
|
self.assertEqual(sponsorship_request.org_website, data["website"])
|
|
|
|
self.assertEqual(sponsorship_request.org_description, data["description"])
|
|
|
|
self.assertEqual(
|
|
|
|
sponsorship_request.org_type,
|
|
|
|
Realm.ORG_TYPES["opensource"]["id"],
|
|
|
|
)
|
|
|
|
|
2020-06-09 12:24:32 +02:00
|
|
|
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")
|
2021-07-09 19:56:55 +02:00
|
|
|
self.assertEqual(message.subject, "Sponsorship request (Open-source project) 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:\nInfinispan is a distributed in-memory", message.body)
|
|
|
|
|
|
|
|
response = self.client_get("/upgrade/")
|
|
|
|
self.assertEqual(response.status_code, 302)
|
2023-11-02 16:34:37 +01:00
|
|
|
self.assertEqual(response["Location"], "/sponsorship/")
|
2020-06-09 12:24:32 +02:00
|
|
|
|
|
|
|
response = self.client_get("/billing/")
|
2023-11-02 16:34:37 +01:00
|
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
self.assertEqual(response["Location"], "/sponsorship/")
|
|
|
|
|
|
|
|
response = self.client_get("/sponsorship/")
|
2023-02-13 20:49:56 +01:00
|
|
|
self.assert_in_success_response(
|
|
|
|
[
|
2023-11-04 04:52:14 +01:00
|
|
|
'This organization has requested sponsorship for a free or discounted <a href="/plans/">Zulip Cloud Standard</a> plan.'
|
2023-02-13 20:49:56 +01:00
|
|
|
],
|
|
|
|
response,
|
|
|
|
)
|
2023-11-02 16:34:37 +01:00
|
|
|
self.assert_in_success_response(
|
|
|
|
[
|
|
|
|
'Please feel free to <a href="mailto:support@zulip.com">contact Zulip support</a> with any questions or updates to your request.'
|
|
|
|
],
|
2023-02-13 20:49:56 +01:00
|
|
|
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
|
|
|
|
2021-10-18 23:28:17 +02:00
|
|
|
user.realm.plan_type = Realm.PLAN_TYPE_STANDARD_FREE
|
2020-08-21 14:45:43 +02:00
|
|
|
user.realm.save()
|
|
|
|
self.login_user(self.example_user("hamlet"))
|
2023-11-02 16:34:37 +01:00
|
|
|
response = self.client_get("/sponsorship/")
|
2021-02-12 08:19:30 +01:00
|
|
|
self.assert_in_success_response(
|
2023-11-04 04:52:14 +01:00
|
|
|
[
|
|
|
|
'Zulip is sponsoring free <a href="/plans/">Zulip Cloud Standard</a> hosting for this organization.'
|
|
|
|
],
|
2023-02-13 20:49:56 +01:00
|
|
|
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/")
|
2023-11-02 16:34:37 +01:00
|
|
|
not_admin_message = (
|
|
|
|
"You must be an organization owner or a billing administrator to view this page."
|
|
|
|
)
|
|
|
|
self.assert_in_success_response([not_admin_message], response)
|
2018-03-31 04:13:44 +02:00
|
|
|
|
2021-10-18 23:28:17 +02:00
|
|
|
user.realm.plan_type = Realm.PLAN_TYPE_STANDARD_FREE
|
2020-08-21 14:45:43 +02:00
|
|
|
user.realm.save()
|
|
|
|
response = self.client_get("/billing/")
|
2023-11-02 16:34:37 +01:00
|
|
|
self.assert_in_success_response([not_admin_message], response)
|
|
|
|
|
|
|
|
# Billing page redirects to sponsorship page for standard free admins.
|
|
|
|
user = self.example_user("hamlet")
|
|
|
|
self.login_user(user)
|
|
|
|
response = self.client_get("/billing/")
|
|
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
self.assertEqual("/sponsorship/", response["Location"])
|
2020-08-21 14:45:43 +02:00
|
|
|
|
2021-10-18 23:28:17 +02:00
|
|
|
user.realm.plan_type = Realm.PLAN_TYPE_LIMITED
|
2020-08-21 14:45:43 +02:00
|
|
|
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)
|
2022-05-29 21:12:13 +02:00
|
|
|
self.assertEqual("/upgrade/", response["Location"])
|
2018-11-05 22:37:22 +01:00
|
|
|
|
2021-12-27 21:25:38 +01:00
|
|
|
def test_upgrade_page_for_demo_organizations(self) -> None:
|
2023-11-02 16:34:37 +01:00
|
|
|
user = self.example_user("hamlet")
|
2021-12-27 21:25:38 +01:00
|
|
|
user.realm.demo_organization_scheduled_deletion_date = timezone_now() + timedelta(days=30)
|
|
|
|
user.realm.save()
|
|
|
|
self.login_user(user)
|
|
|
|
|
|
|
|
response = self.client_get("/billing/", follow=True)
|
2022-08-29 21:16:51 +02:00
|
|
|
self.assert_in_success_response(["cannot be directly upgraded"], response)
|
2021-12-27 21:25:38 +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)
|
|
|
|
|
2021-10-18 23:28:17 +02:00
|
|
|
user.realm.plan_type = Realm.PLAN_TYPE_STANDARD_FREE
|
2020-08-21 14:45:43 +02:00
|
|
|
user.realm.save()
|
|
|
|
response = self.client_get("/upgrade/")
|
|
|
|
self.assertEqual(response.status_code, 302)
|
2023-11-02 16:34:37 +01:00
|
|
|
self.assertEqual(response["Location"], "/sponsorship/")
|
2020-08-21 14:45:43 +02:00
|
|
|
|
2021-10-18 23:28:17 +02:00
|
|
|
user.realm.plan_type = Realm.PLAN_TYPE_LIMITED
|
2020-08-21 14:45:43 +02:00
|
|
|
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)
|
2022-05-29 21:12:13 +02:00
|
|
|
self.assertEqual(response["Location"], "/billing/")
|
2020-05-22 15:42:46 +02:00
|
|
|
|
|
|
|
with self.settings(FREE_TRIAL_DAYS=30):
|
|
|
|
response = self.client_get("/upgrade/")
|
|
|
|
self.assertEqual(response.status_code, 302)
|
2022-05-29 21:12:13 +02:00
|
|
|
self.assertEqual(response["Location"], "/billing/")
|
2020-05-22 15:42:46 +02:00
|
|
|
|
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)
|
2022-05-29 21:12:13 +02:00
|
|
|
self.assertEqual(response["Location"], "/billing/?onboarding=true")
|
2020-05-22 15:42:46 +02:00
|
|
|
|
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")
|
2023-10-26 14:11:43 +02:00
|
|
|
# billing_session = RealmBillingSession(user)
|
|
|
|
# billing_session.create_stripe_customer()
|
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()
|
2021-08-29 15:33:29 +02:00
|
|
|
def test_replace_payment_method(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
|
2021-08-29 15:33:29 +02:00
|
|
|
customer = Customer.objects.first()
|
|
|
|
assert customer is not None
|
|
|
|
stripe_customer_id = customer.stripe_customer_id
|
2021-07-24 16:56:39 +02:00
|
|
|
assert stripe_customer_id is not None
|
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
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
start_session_json_response = self.client_post(
|
|
|
|
"/json/billing/session/start_card_update_session"
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(start_session_json_response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "card_update_from_billing_page",
|
|
|
|
"status": "created",
|
|
|
|
},
|
|
|
|
)
|
|
|
|
with self.assertRaises(stripe.error.CardError):
|
|
|
|
# We don't have to handle this since the Stripe Checkout page would
|
|
|
|
# ask Customer to enter a valid card number. trigger_stripe_checkout_session_completed_webhook
|
|
|
|
# emulates what happens in the Stripe Checkout page. Adding this check mostly for coverage of
|
|
|
|
# create_payment_method.
|
|
|
|
self.trigger_stripe_checkout_session_completed_webhook(
|
|
|
|
create_payment_method(self.get_test_card_number(attaches_to_customer=False))
|
|
|
|
)
|
2019-04-04 10:02:49 +02:00
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
start_session_json_response = self.client_post(
|
|
|
|
"/json/billing/session/start_card_update_session"
|
|
|
|
)
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(start_session_json_response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "card_update_from_billing_page",
|
|
|
|
"status": "created",
|
|
|
|
},
|
|
|
|
)
|
2021-02-12 08:20:45 +01:00
|
|
|
with self.assertLogs("corporate.stripe", "INFO") as m:
|
2021-08-29 15:33:29 +02:00
|
|
|
self.trigger_stripe_checkout_session_completed_webhook(
|
|
|
|
create_payment_method(
|
|
|
|
self.get_test_card_number(attaches_to_customer=True, charge_succeeds=False)
|
|
|
|
)
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
|
|
|
self.assertEqual(
|
2021-08-29 15:33:29 +02:00
|
|
|
m.output[0],
|
|
|
|
"INFO:corporate.stripe:Stripe card error: 402 card_error card_declined None",
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(start_session_json_response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "card_update_from_billing_page",
|
|
|
|
"status": "completed",
|
|
|
|
"event_handler": {
|
|
|
|
"status": "failed",
|
|
|
|
"error": {"message": "Your card was declined.", "description": "card error"},
|
|
|
|
},
|
|
|
|
},
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2021-08-29 15:33:29 +02:00
|
|
|
|
|
|
|
response = self.client_get("/billing/")
|
|
|
|
self.assert_in_success_response(["payment method: <strong>visa ending in 0341"], response)
|
|
|
|
assert RealmAuditLog.objects.filter(event_type=RealmAuditLog.STRIPE_CARD_CHANGED).exists()
|
|
|
|
stripe_payment_methods = stripe.PaymentMethod.list(customer=stripe_customer_id, type="card")
|
|
|
|
self.assert_length(stripe_payment_methods, 2)
|
|
|
|
|
|
|
|
for stripe_payment_method in stripe_payment_methods:
|
|
|
|
stripe.PaymentMethod.detach(stripe_payment_method.id)
|
|
|
|
response = self.client_get("/billing/")
|
|
|
|
self.assert_in_success_response(
|
|
|
|
["payment method: <strong>No payment method on file"], response
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2019-04-04 10:02:49 +02:00
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
start_session_json_response = self.client_post(
|
|
|
|
"/json/billing/session/start_card_update_session"
|
|
|
|
)
|
|
|
|
self.assert_json_success(start_session_json_response)
|
|
|
|
self.trigger_stripe_checkout_session_completed_webhook(
|
|
|
|
create_payment_method(
|
|
|
|
self.get_test_card_number(
|
|
|
|
attaches_to_customer=True, charge_succeeds=True, card_provider="mastercard"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict = self.assert_json_success(start_session_json_response)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_details_of_valid_session_from_event_status_endpoint(
|
2022-06-07 01:37:01 +02:00
|
|
|
response_dict["stripe_session_id"],
|
2021-08-29 15:33:29 +02:00
|
|
|
{
|
|
|
|
"type": "card_update_from_billing_page",
|
|
|
|
"status": "completed",
|
|
|
|
"event_handler": {"status": "succeeded"},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
self.login_user(self.example_user("iago"))
|
|
|
|
response = self.client_get(
|
|
|
|
"/json/billing/event/status",
|
2022-06-07 01:37:01 +02:00
|
|
|
{"stripe_session_id": response_dict["stripe_session_id"]},
|
2021-08-29 15:33:29 +02:00
|
|
|
)
|
|
|
|
self.assert_json_error_contains(
|
|
|
|
response, "Must be a billing administrator or an organization owner"
|
|
|
|
)
|
|
|
|
|
|
|
|
self.login_user(self.example_user("hamlet"))
|
|
|
|
response = self.client_get("/billing/")
|
|
|
|
self.assert_in_success_response(
|
|
|
|
["payment method: <strong>mastercard ending in 4444"], response
|
|
|
|
)
|
|
|
|
self.assert_length(stripe.PaymentMethod.list(customer=stripe_customer_id, type="card"), 1)
|
2021-07-20 13:28:54 +02:00
|
|
|
# Ideally we'd also test that we don't pay invoices with collection_method=='send_invoice'
|
2019-04-04 10:02:49 +02:00
|
|
|
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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
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:
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-17 16:35:33 +01:00
|
|
|
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
|
|
|
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2021-08-29 15:33:29 +02:00
|
|
|
mock_customer = Mock(email=user.delivery_email)
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch(
|
|
|
|
"corporate.views.billing_page.stripe_get_customer", return_value=mock_customer
|
|
|
|
):
|
2020-12-30 15:53:43 +01:00
|
|
|
response = self.client_get("/billing/")
|
2021-01-25 19:13:52 +01:00
|
|
|
self.assert_in_success_response(
|
|
|
|
[
|
2023-09-06 18:52:48 +02:00
|
|
|
"Your plan will be downgraded to <strong>Zulip Free</strong> on "
|
2021-01-25 19:13:52 +01:00
|
|
|
"<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-07-24 16:56:39 +02:00
|
|
|
plan = CustomerPlan.objects.first()
|
|
|
|
assert plan is not None
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(get_realm("zulip").plan_type, Realm.PLAN_TYPE_LIMITED)
|
2021-07-24 16:56:39 +02:00
|
|
|
self.assertEqual(plan.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
|
2021-07-24 16:56:39 +02:00
|
|
|
plan = CustomerPlan.objects.first()
|
|
|
|
assert plan is not None
|
|
|
|
self.assertIsNotNone(plan.next_invoice_date)
|
2019-04-08 05:16:35 +02:00
|
|
|
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
|
2021-07-24 16:56:39 +02:00
|
|
|
plan = CustomerPlan.objects.first()
|
|
|
|
assert plan is not None
|
|
|
|
self.assertIsNone(plan.next_invoice_date)
|
2019-04-08 05:16:35 +02:00
|
|
|
|
|
|
|
# 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:
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-17 16:35:33 +01:00
|
|
|
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-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.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)
|
2023-07-13 19:46:06 +02:00
|
|
|
self.assertEqual(audit_log.extra_data["monthly_plan_id"], monthly_plan.id)
|
|
|
|
self.assertEqual(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,
|
2022-02-05 08:29:54 +01:00
|
|
|
"description": "Zulip Cloud Standard - renewal",
|
2021-02-12 08:19:30 +01:00
|
|
|
"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,
|
2022-02-05 08:29:54 +01:00
|
|
|
"description": "Zulip Cloud 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:
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-17 16:35:33 +01:00
|
|
|
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-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.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,
|
2022-02-05 08:29:54 +01:00
|
|
|
"description": "Zulip Cloud 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(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,
|
2022-02-05 08:29:54 +01:00
|
|
|
"description": "Zulip Cloud 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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2021-02-12 08:20:45 +01:00
|
|
|
with self.assertLogs("corporate.stripe", "INFO") as m:
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-17 16:35:33 +01:00
|
|
|
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-07-24 16:56:39 +02:00
|
|
|
plan = CustomerPlan.objects.first()
|
|
|
|
assert plan is not None
|
|
|
|
self.assertEqual(plan.status, CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE)
|
2021-02-12 08:20:45 +01:00
|
|
|
with self.assertLogs("corporate.stripe", "INFO") as m:
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-17 16:35:33 +01:00
|
|
|
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)
|
2021-07-24 16:56:39 +02:00
|
|
|
plan = CustomerPlan.objects.first()
|
|
|
|
assert plan is not None
|
|
|
|
self.assertEqual(plan.status, CustomerPlan.ACTIVE)
|
2020-04-23 20:10:15 +02:00
|
|
|
|
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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2021-02-12 08:20:45 +01:00
|
|
|
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
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-17 16:35:33 +01:00
|
|
|
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()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert plan is not None
|
2019-04-08 05:16:35 +02:00
|
|
|
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()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert plan is not None
|
2019-04-08 05:16:35 +02:00
|
|
|
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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, True)
|
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-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(get_realm("zulip").plan_type, Realm.PLAN_TYPE_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()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert last_ledger_entry is not None
|
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
|
|
|
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-17 16:35:33 +01:00
|
|
|
self.client_patch("/json/billing/plan", {"status": CustomerPlan.ENDED})
|
2020-04-23 20:10:15 +02:00
|
|
|
|
|
|
|
plan.refresh_from_db()
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(get_realm("zulip").plan_type, Realm.PLAN_TYPE_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()
|
|
|
|
|
2021-08-14 01:01:37 +02:00
|
|
|
def test_reupgrade_by_billing_admin_after_downgrade(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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
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:
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-17 16:35:33 +01:00
|
|
|
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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2021-02-04 11:20:53 +01:00
|
|
|
self.assertEqual(
|
2021-02-12 08:19:30 +01:00
|
|
|
m.output[0],
|
2023-10-30 22:29:22 +01:00
|
|
|
"WARNING:corporate.stripe:Upgrade of <Realm: zulip 2> (with stripe_customer_id: cus_123) failed because of existing active plan.",
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2021-07-04 08:19:18 +02:00
|
|
|
self.assertEqual(
|
|
|
|
context.exception.error_description, "subscribing with existing subscription"
|
|
|
|
)
|
2020-04-03 16:52:35 +02:00
|
|
|
|
|
|
|
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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
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()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert current_plan is not None
|
2020-04-03 16:52:35 +02:00
|
|
|
next_invoice_date = add_months(self.next_year, 1)
|
|
|
|
self.assertEqual(current_plan.next_invoice_date, next_invoice_date)
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(get_realm("zulip").plan_type, Realm.PLAN_TYPE_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()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert old_plan is not None
|
2020-04-03 16:52:35 +02:00
|
|
|
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)
|
|
|
|
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-23 17:08:27 +01:00
|
|
|
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."
|
|
|
|
)
|
|
|
|
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-23 17:08:27 +01:00
|
|
|
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."
|
|
|
|
)
|
|
|
|
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-23 17:08:27 +01:00
|
|
|
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."
|
|
|
|
)
|
|
|
|
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-23 17:08:27 +01:00
|
|
|
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.")
|
|
|
|
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-23 17:08:27 +01:00
|
|
|
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."
|
|
|
|
)
|
|
|
|
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-23 17:08:27 +01:00
|
|
|
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(
|
2021-07-25 16:31:12 +02:00
|
|
|
assert_is_not_none(Customer.objects.get(realm=user.realm).stripe_customer_id)
|
2020-12-23 17:08:27 +01:00
|
|
|
)
|
|
|
|
[invoice, _] = stripe.Invoice.list(customer=stripe_customer.id)
|
|
|
|
invoice_params = {
|
2023-09-12 21:10:57 +02:00
|
|
|
"amount_due": 8000 * 150 + 8000 * 50,
|
2020-12-23 17:08:27 +01:00
|
|
|
"amount_paid": 0,
|
|
|
|
"attempt_count": 0,
|
|
|
|
"auto_advance": True,
|
2021-07-20 13:28:54 +02:00
|
|
|
"collection_method": "send_invoice",
|
2022-02-05 08:29:54 +01:00
|
|
|
"statement_descriptor": "Zulip Cloud Standard",
|
2020-12-23 17:08:27 +01:00
|
|
|
"status": "open",
|
2023-09-12 21:10:57 +02:00
|
|
|
"total": 8000 * 150 + 8000 * 50,
|
2020-12-23 17:08:27 +01:00
|
|
|
}
|
|
|
|
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,
|
2022-02-05 08:29:54 +01:00
|
|
|
"description": "Zulip Cloud Standard - renewal",
|
2020-12-23 17:08:27 +01:00
|
|
|
"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)
|
|
|
|
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.next_year):
|
2020-12-23 17:08:27 +01:00
|
|
|
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(
|
2021-07-25 16:31:12 +02:00
|
|
|
assert_is_not_none(Customer.objects.get(realm=user.realm).stripe_customer_id)
|
2020-12-23 17:08:27 +01:00
|
|
|
)
|
|
|
|
[invoice, _, _] = stripe.Invoice.list(customer=stripe_customer.id)
|
|
|
|
invoice_params = {
|
|
|
|
"amount_due": 8000 * 120,
|
|
|
|
"amount_paid": 0,
|
|
|
|
"attempt_count": 0,
|
|
|
|
"auto_advance": True,
|
2021-07-20 13:28:54 +02:00
|
|
|
"collection_method": "send_invoice",
|
2022-02-05 08:29:54 +01:00
|
|
|
"statement_descriptor": "Zulip Cloud Standard",
|
2020-12-23 17:08:27 +01:00
|
|
|
"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,
|
2022-02-05 08:29:54 +01:00
|
|
|
"description": "Zulip Cloud Standard - renewal",
|
2020-12-23 17:08:27 +01:00
|
|
|
"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)
|
|
|
|
|
2023-09-26 01:57:58 +02:00
|
|
|
def test_update_licenses_of_manual_plan_from_billing_page_exempt_from_license_number_check(
|
|
|
|
self,
|
|
|
|
) -> None:
|
|
|
|
"""
|
|
|
|
Verifies that an organization exempt from the license number check can reduce their number
|
|
|
|
of licenses.
|
|
|
|
"""
|
|
|
|
user = self.example_user("hamlet")
|
|
|
|
self.login_user(user)
|
|
|
|
|
|
|
|
customer = Customer.objects.get_or_create(realm=user.realm)[0]
|
|
|
|
customer.exempt_from_license_number_check = True
|
|
|
|
customer.save()
|
|
|
|
|
|
|
|
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
|
|
|
self.local_upgrade(100, False, CustomerPlan.ANNUAL, True, False)
|
|
|
|
|
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
|
|
|
result = self.client_patch(
|
|
|
|
"/json/billing/plan",
|
|
|
|
{"licenses_at_next_renewal": get_latest_seat_count(user.realm) - 2},
|
|
|
|
)
|
|
|
|
|
|
|
|
self.assert_json_success(result)
|
|
|
|
latest_license_ledger = LicenseLedger.objects.last()
|
|
|
|
assert latest_license_ledger is not None
|
|
|
|
self.assertEqual(
|
|
|
|
latest_license_ledger.licenses_at_next_renewal, get_latest_seat_count(user.realm) - 2
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_upgrade_exempt_from_license_number_check_realm_less_licenses_than_seat_count(
|
|
|
|
self,
|
|
|
|
) -> None:
|
|
|
|
"""
|
|
|
|
Verifies that an organization exempt from the license number check can upgrade their plan,
|
|
|
|
specifying a number of licenses less than their current number of licenses and be charged
|
|
|
|
for the number of licenses specified. Tests against a former bug, where the organization
|
|
|
|
was charged for the current seat count, despite specifying a lower number of licenses.
|
|
|
|
"""
|
|
|
|
user = self.example_user("hamlet")
|
|
|
|
self.login_user(user)
|
|
|
|
|
|
|
|
customer = Customer.objects.get_or_create(realm=user.realm)[0]
|
|
|
|
customer.exempt_from_license_number_check = True
|
|
|
|
customer.save()
|
|
|
|
|
|
|
|
reduced_seat_count = get_latest_seat_count(user.realm) - 2
|
|
|
|
|
|
|
|
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
|
|
|
self.local_upgrade(reduced_seat_count, False, CustomerPlan.ANNUAL, True, False)
|
|
|
|
|
|
|
|
latest_license_ledger = LicenseLedger.objects.last()
|
|
|
|
assert latest_license_ledger is not None
|
|
|
|
self.assertEqual(latest_license_ledger.licenses_at_next_renewal, reduced_seat_count)
|
|
|
|
self.assertEqual(latest_license_ledger.licenses, reduced_seat_count)
|
|
|
|
|
2021-08-14 01:01:37 +02:00
|
|
|
def test_update_licenses_of_automatic_plan_from_billing_page(self) -> None:
|
2020-12-23 17:08:27 +01:00
|
|
|
user = self.example_user("hamlet")
|
|
|
|
self.login_user(user)
|
|
|
|
|
|
|
|
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
2021-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2020-12-23 17:08:27 +01:00
|
|
|
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-23 17:08:27 +01:00
|
|
|
result = self.client_patch("/json/billing/plan", {"licenses": 100})
|
|
|
|
self.assert_json_error_contains(result, "Your plan is on automatic license management.")
|
|
|
|
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-23 17:08:27 +01:00
|
|
|
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):
|
2021-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2021-04-14 15:50:40 +02:00
|
|
|
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):
|
2021-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2020-12-10 18:15:09 +01:00
|
|
|
|
|
|
|
self.login_user(self.example_user("hamlet"))
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-17 16:35:33 +01:00
|
|
|
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):
|
2021-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2020-12-17 16:35:33 +01:00
|
|
|
|
|
|
|
self.login_user(self.example_user("hamlet"))
|
|
|
|
with self.assertLogs("corporate.stripe", "INFO") as m:
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-17 16:35:33 +01:00
|
|
|
result = self.client_patch(
|
|
|
|
"/json/billing/plan", {"status": CustomerPlan.DOWNGRADE_AT_END_OF_CYCLE}
|
|
|
|
)
|
|
|
|
self.assert_json_success(result)
|
2021-09-22 06:08:34 +02:00
|
|
|
self.assertRegex(
|
2020-12-17 16:35:33 +01:00
|
|
|
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):
|
2021-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.MONTHLY, True, False)
|
2020-12-17 16:35:33 +01:00
|
|
|
|
|
|
|
self.login_user(self.example_user("hamlet"))
|
|
|
|
with self.assertLogs("corporate.stripe", "INFO") as m:
|
2021-07-15 16:38:37 +02:00
|
|
|
with patch("corporate.views.billing_page.timezone_now", return_value=self.now):
|
2020-12-17 16:35:33 +01:00
|
|
|
result = self.client_patch(
|
|
|
|
"/json/billing/plan", {"status": CustomerPlan.SWITCH_TO_ANNUAL_AT_END_OF_CYCLE}
|
|
|
|
)
|
|
|
|
self.assert_json_success(result)
|
2021-09-22 06:08:34 +02:00
|
|
|
self.assertRegex(
|
2020-12-17 16:35:33 +01:00
|
|
|
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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2020-03-20 14:58:38 +01:00
|
|
|
|
|
|
|
plan = CustomerPlan.objects.get()
|
|
|
|
self.assertEqual(plan.next_invoice_date, self.next_month)
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(get_realm("zulip").plan_type, Realm.PLAN_TYPE_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()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert last_ledger_entry is not None
|
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)
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(get_realm("zulip").plan_type, Realm.PLAN_TYPE_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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
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()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert current_plan is not None
|
2020-04-03 16:52:35 +02:00
|
|
|
self.assertEqual(current_plan.next_invoice_date, self.next_month)
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(get_realm("zulip").plan_type, Realm.PLAN_TYPE_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()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert old_plan is not None
|
2020-04-03 16:52:35 +02:00
|
|
|
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)
|
|
|
|
|
2023-10-26 14:11:43 +02:00
|
|
|
zulip_customer = RealmBillingSession(iago).update_or_create_stripe_customer()
|
|
|
|
lear_customer = RealmBillingSession(king).update_or_create_stripe_customer()
|
2021-06-18 21:10:45 +02:00
|
|
|
|
|
|
|
assert zulip_customer.stripe_customer_id
|
|
|
|
stripe.InvoiceItem.create(
|
|
|
|
currency="usd",
|
|
|
|
customer=zulip_customer.stripe_customer_id,
|
2022-02-05 08:31:59 +01:00
|
|
|
description="Zulip Cloud Standard upgrade",
|
2021-06-18 21:10:45 +02:00
|
|
|
discountable=False,
|
|
|
|
unit_amount=800,
|
|
|
|
quantity=8,
|
|
|
|
)
|
|
|
|
stripe_invoice = stripe.Invoice.create(
|
|
|
|
auto_advance=True,
|
2021-07-20 13:28:54 +02:00
|
|
|
collection_method="send_invoice",
|
2021-06-18 21:10:45 +02:00
|
|
|
customer=zulip_customer.stripe_customer_id,
|
|
|
|
days_until_due=30,
|
2022-02-05 08:29:54 +01:00
|
|
|
statement_descriptor="Zulip Cloud Standard",
|
2021-06-18 21:10:45 +02:00
|
|
|
)
|
|
|
|
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,
|
2022-02-05 08:31:59 +01:00
|
|
|
description="Zulip Cloud Standard upgrade",
|
2020-08-13 10:39:25 +02:00
|
|
|
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,
|
2021-07-20 13:28:54 +02:00
|
|
|
collection_method="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,
|
2022-02-05 08:29:54 +01:00
|
|
|
statement_descriptor="Zulip Cloud 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")
|
|
|
|
|
2021-07-21 10:05:23 +02:00
|
|
|
def create_invoices(self, customer: Customer, num_invoices: int) -> List[stripe.Invoice]:
|
|
|
|
invoices = []
|
|
|
|
assert customer.stripe_customer_id is not None
|
|
|
|
for _ in range(num_invoices):
|
|
|
|
stripe.InvoiceItem.create(
|
|
|
|
amount=10000,
|
|
|
|
currency="usd",
|
|
|
|
customer=customer.stripe_customer_id,
|
2022-02-05 08:31:59 +01:00
|
|
|
description="Zulip Cloud Standard",
|
2021-07-21 10:05:23 +02:00
|
|
|
discountable=False,
|
|
|
|
)
|
|
|
|
invoice = stripe.Invoice.create(
|
|
|
|
auto_advance=True,
|
2021-07-20 13:28:54 +02:00
|
|
|
collection_method="send_invoice",
|
2021-07-21 10:05:23 +02:00
|
|
|
customer=customer.stripe_customer_id,
|
|
|
|
days_until_due=DEFAULT_INVOICE_DAYS_UNTIL_DUE,
|
2022-02-05 08:29:54 +01:00
|
|
|
statement_descriptor="Zulip Cloud Standard",
|
2021-07-21 10:05:23 +02:00
|
|
|
)
|
|
|
|
stripe.Invoice.finalize_invoice(invoice)
|
|
|
|
invoices.append(invoice)
|
|
|
|
return invoices
|
|
|
|
|
2021-06-11 12:53:45 +02:00
|
|
|
@mock_stripe()
|
|
|
|
def test_downgrade_small_realms_behind_on_payments_as_needed(self, *mock: Mock) -> None:
|
|
|
|
def create_realm(
|
|
|
|
users_to_create: int,
|
|
|
|
create_stripe_customer: bool,
|
|
|
|
create_plan: bool,
|
2021-07-21 10:05:23 +02:00
|
|
|
num_invoices: Optional[int] = None,
|
|
|
|
) -> Tuple[Realm, Optional[Customer], Optional[CustomerPlan], List[stripe.Invoice]]:
|
2021-06-11 12:53:45 +02:00
|
|
|
realm_string_id = "realm_" + str(random.randrange(1, 1000000))
|
2023-07-06 14:27:14 +02:00
|
|
|
realm = do_create_realm(
|
|
|
|
string_id=realm_string_id,
|
|
|
|
name=realm_string_id,
|
|
|
|
plan_type=Realm.PLAN_TYPE_SELF_HOSTED,
|
|
|
|
)
|
2021-06-11 12:53:45 +02:00
|
|
|
users = []
|
|
|
|
for i in range(users_to_create):
|
|
|
|
user = UserProfile.objects.create(
|
|
|
|
delivery_email=f"user-{i}-{realm_string_id}@zulip.com",
|
|
|
|
email=f"user-{i}-{realm_string_id}@zulip.com",
|
|
|
|
realm=realm,
|
|
|
|
)
|
|
|
|
users.append(user)
|
|
|
|
|
|
|
|
customer = None
|
|
|
|
if create_stripe_customer:
|
2023-10-26 14:11:43 +02:00
|
|
|
billable_user = RealmBillingSession(users[0])
|
|
|
|
customer = billable_user.create_stripe_customer()
|
2021-06-11 12:53:45 +02:00
|
|
|
plan = None
|
|
|
|
if create_plan:
|
|
|
|
plan, _ = self.subscribe_realm_to_monthly_plan_on_manual_license_management(
|
|
|
|
realm, users_to_create, users_to_create
|
|
|
|
)
|
|
|
|
invoices = []
|
2021-07-21 10:05:23 +02:00
|
|
|
if num_invoices is not None:
|
|
|
|
assert customer is not None
|
|
|
|
invoices = self.create_invoices(customer, num_invoices)
|
|
|
|
return realm, customer, plan, invoices
|
2021-06-11 12:53:45 +02:00
|
|
|
|
2021-08-03 06:29:32 +02:00
|
|
|
@dataclass
|
|
|
|
class Row:
|
2021-07-21 08:56:21 +02:00
|
|
|
realm: Realm
|
|
|
|
expected_plan_type: int
|
|
|
|
plan: Optional[CustomerPlan]
|
|
|
|
expected_plan_status: Optional[int]
|
|
|
|
void_all_open_invoices_mock_called: bool
|
|
|
|
email_expected_to_be_sent: bool
|
|
|
|
|
|
|
|
rows: List[Row] = []
|
2021-07-21 10:05:23 +02:00
|
|
|
realm, _, _, _ = create_realm(
|
2021-07-16 15:35:13 +02:00
|
|
|
users_to_create=1, create_stripe_customer=False, create_plan=False
|
|
|
|
)
|
|
|
|
# To create local Customer object but no Stripe customer.
|
2023-10-31 19:22:55 +01:00
|
|
|
attach_discount_to_realm(realm, Decimal(20), acting_user=self.example_user("iago"))
|
2021-10-18 23:28:17 +02:00
|
|
|
rows.append(Row(realm, Realm.PLAN_TYPE_SELF_HOSTED, None, None, False, False))
|
2021-07-16 15:35:13 +02:00
|
|
|
|
2021-07-21 10:05:23 +02:00
|
|
|
realm, _, _, _ = create_realm(
|
2021-06-11 12:53:45 +02:00
|
|
|
users_to_create=1, create_stripe_customer=True, create_plan=False
|
|
|
|
)
|
2021-10-18 23:28:17 +02:00
|
|
|
rows.append(Row(realm, Realm.PLAN_TYPE_SELF_HOSTED, None, None, False, False))
|
2021-06-11 12:53:45 +02:00
|
|
|
|
2021-07-16 17:13:49 +02:00
|
|
|
realm, customer, _, _ = create_realm(
|
|
|
|
users_to_create=1, create_stripe_customer=True, create_plan=False, num_invoices=1
|
|
|
|
)
|
2021-10-18 23:28:17 +02:00
|
|
|
rows.append(Row(realm, Realm.PLAN_TYPE_SELF_HOSTED, None, None, True, False))
|
2021-07-16 17:13:49 +02:00
|
|
|
|
2021-07-21 10:05:23 +02:00
|
|
|
realm, _, plan, _ = create_realm(
|
2021-06-11 12:53:45 +02:00
|
|
|
users_to_create=1, create_stripe_customer=True, create_plan=True
|
|
|
|
)
|
2021-10-18 23:28:17 +02:00
|
|
|
rows.append(Row(realm, Realm.PLAN_TYPE_STANDARD, plan, CustomerPlan.ACTIVE, False, False))
|
2021-06-11 12:53:45 +02:00
|
|
|
|
2021-07-21 10:05:23 +02:00
|
|
|
realm, customer, plan, _ = create_realm(
|
|
|
|
users_to_create=1, create_stripe_customer=True, create_plan=True, num_invoices=1
|
2021-06-11 12:53:45 +02:00
|
|
|
)
|
2021-10-18 23:28:17 +02:00
|
|
|
rows.append(Row(realm, Realm.PLAN_TYPE_STANDARD, plan, CustomerPlan.ACTIVE, False, False))
|
2021-06-11 12:53:45 +02:00
|
|
|
|
2021-07-21 10:05:23 +02:00
|
|
|
realm, customer, plan, _ = create_realm(
|
|
|
|
users_to_create=3, create_stripe_customer=True, create_plan=True, num_invoices=2
|
2021-06-11 12:53:45 +02:00
|
|
|
)
|
2021-10-18 23:28:17 +02:00
|
|
|
rows.append(Row(realm, Realm.PLAN_TYPE_LIMITED, plan, CustomerPlan.ENDED, True, True))
|
2021-06-11 12:53:45 +02:00
|
|
|
|
2021-07-21 10:05:23 +02:00
|
|
|
realm, customer, plan, invoices = create_realm(
|
|
|
|
users_to_create=1, create_stripe_customer=True, create_plan=True, num_invoices=2
|
2021-06-11 12:53:45 +02:00
|
|
|
)
|
2021-07-21 08:56:21 +02:00
|
|
|
for invoice in invoices:
|
2021-06-11 12:53:45 +02:00
|
|
|
stripe.Invoice.pay(invoice, paid_out_of_band=True)
|
2021-10-18 23:28:17 +02:00
|
|
|
rows.append(Row(realm, Realm.PLAN_TYPE_STANDARD, plan, CustomerPlan.ACTIVE, False, False))
|
2021-06-11 12:53:45 +02:00
|
|
|
|
2021-07-21 10:05:23 +02:00
|
|
|
realm, customer, plan, _ = create_realm(
|
|
|
|
users_to_create=20, create_stripe_customer=True, create_plan=True, num_invoices=2
|
2021-06-11 12:53:45 +02:00
|
|
|
)
|
2021-10-18 23:28:17 +02:00
|
|
|
rows.append(Row(realm, Realm.PLAN_TYPE_STANDARD, plan, CustomerPlan.ACTIVE, False, False))
|
2021-06-11 12:53:45 +02:00
|
|
|
|
2023-10-30 16:28:52 +01:00
|
|
|
# Customer objects without a realm should be excluded from query.
|
|
|
|
remote_server = RemoteZulipServer.objects.create(
|
|
|
|
uuid=str(uuid.uuid4()),
|
|
|
|
api_key="magic_secret_api_key",
|
|
|
|
hostname="demo.example.com",
|
|
|
|
contact_email="email@example.com",
|
|
|
|
)
|
|
|
|
Customer.objects.create(remote_server=remote_server, stripe_customer_id="cus_xxx")
|
|
|
|
|
2021-06-11 12:53:45 +02:00
|
|
|
with patch("corporate.lib.stripe.void_all_open_invoices") as void_all_open_invoices_mock:
|
|
|
|
downgrade_small_realms_behind_on_payments_as_needed()
|
|
|
|
|
|
|
|
from django.core.mail import outbox
|
|
|
|
|
2021-07-21 08:56:21 +02:00
|
|
|
for row in rows:
|
|
|
|
row.realm.refresh_from_db()
|
|
|
|
self.assertEqual(row.realm.plan_type, row.expected_plan_type)
|
|
|
|
if row.plan is not None:
|
|
|
|
row.plan.refresh_from_db()
|
|
|
|
self.assertEqual(row.plan.status, row.expected_plan_status)
|
|
|
|
if row.void_all_open_invoices_mock_called:
|
|
|
|
void_all_open_invoices_mock.assert_any_call(row.realm)
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
void_all_open_invoices_mock.assert_any_call(row.realm)
|
|
|
|
except AssertionError:
|
|
|
|
pass
|
|
|
|
else: # nocoverage
|
|
|
|
raise AssertionError("void_all_open_invoices_mock should not be called")
|
|
|
|
|
|
|
|
email_found = False
|
|
|
|
for email in outbox:
|
|
|
|
recipient = UserProfile.objects.get(email=email.to[0])
|
|
|
|
if recipient.realm == row.realm:
|
|
|
|
self.assertIn(
|
|
|
|
f"Your organization, http://{row.realm.string_id}.testserver, has been downgraded",
|
|
|
|
outbox[0].body,
|
|
|
|
)
|
|
|
|
self.assert_length(email.to, 1)
|
|
|
|
self.assertTrue(recipient.is_billing_admin)
|
|
|
|
email_found = True
|
|
|
|
self.assertEqual(row.email_expected_to_be_sent, email_found)
|
2021-06-11 12:53:45 +02:00
|
|
|
|
2021-09-21 21:21:03 +02:00
|
|
|
@mock_stripe()
|
|
|
|
def test_switch_realm_from_standard_to_plus_plan(self, *mock: Mock) -> None:
|
2021-10-30 18:54:33 +02:00
|
|
|
iago = self.example_user("iago")
|
|
|
|
realm = iago.realm
|
2021-09-21 21:21:03 +02:00
|
|
|
|
|
|
|
# Test upgrading to Plus when realm has no Standard subscription
|
|
|
|
with self.assertRaises(BillingError) as billing_context:
|
|
|
|
switch_realm_from_standard_to_plus_plan(realm)
|
|
|
|
self.assertEqual(
|
|
|
|
"Organization does not have an active Standard plan",
|
|
|
|
billing_context.exception.error_description,
|
|
|
|
)
|
|
|
|
|
|
|
|
plan, ledger = self.subscribe_realm_to_manual_license_management_plan(
|
|
|
|
realm, 9, 9, CustomerPlan.MONTHLY
|
|
|
|
)
|
|
|
|
# Test upgrading to Plus when realm has no stripe_customer_id
|
|
|
|
with self.assertRaises(BillingError) as billing_context:
|
|
|
|
switch_realm_from_standard_to_plus_plan(realm)
|
|
|
|
self.assertEqual(
|
|
|
|
"Organization missing Stripe customer.", billing_context.exception.error_description
|
|
|
|
)
|
|
|
|
|
2021-10-30 18:54:33 +02:00
|
|
|
king = self.lear_user("king")
|
|
|
|
realm = king.realm
|
2023-10-26 14:11:43 +02:00
|
|
|
customer = RealmBillingSession(king).update_or_create_stripe_customer()
|
2021-10-30 18:54:33 +02:00
|
|
|
plan = CustomerPlan.objects.create(
|
|
|
|
customer=customer,
|
|
|
|
automanage_licenses=True,
|
|
|
|
billing_cycle_anchor=timezone_now(),
|
|
|
|
billing_schedule=CustomerPlan.MONTHLY,
|
|
|
|
tier=CustomerPlan.STANDARD,
|
|
|
|
)
|
|
|
|
ledger = LicenseLedger.objects.create(
|
|
|
|
plan=plan,
|
|
|
|
is_renewal=True,
|
|
|
|
event_time=timezone_now(),
|
|
|
|
licenses=9,
|
|
|
|
licenses_at_next_renewal=9,
|
|
|
|
)
|
|
|
|
realm.plan_type = Realm.PLAN_TYPE_STANDARD
|
|
|
|
realm.save(update_fields=["plan_type"])
|
2021-09-21 21:21:03 +02:00
|
|
|
plan.invoiced_through = ledger
|
2021-10-30 18:54:33 +02:00
|
|
|
plan.price_per_license = get_price_per_license(CustomerPlan.STANDARD, CustomerPlan.MONTHLY)
|
|
|
|
plan.save(update_fields=["invoiced_through", "price_per_license"])
|
2021-09-21 21:21:03 +02:00
|
|
|
|
|
|
|
switch_realm_from_standard_to_plus_plan(realm)
|
|
|
|
|
|
|
|
plan.refresh_from_db()
|
|
|
|
self.assertEqual(plan.status, CustomerPlan.ENDED)
|
|
|
|
plus_plan = get_current_plan_by_realm(realm)
|
|
|
|
assert plus_plan is not None
|
|
|
|
self.assertEqual(plus_plan.tier, CustomerPlan.PLUS)
|
|
|
|
self.assertEqual(LicenseLedger.objects.filter(plan=plus_plan).count(), 1)
|
|
|
|
|
2023-02-08 12:30:28 +01:00
|
|
|
realm.refresh_from_db()
|
|
|
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_PLUS)
|
|
|
|
|
2021-10-30 18:54:33 +02:00
|
|
|
# There are 9 licenses and the realm is on the Standard monthly plan.
|
|
|
|
# Therefore, the customer has already paid 800 * 9 = 7200 = $72 for
|
|
|
|
# the month. Once they upgrade to Plus, the new price for their 9
|
|
|
|
# licenses will be 1600 * 9 = 14400 = $144. Since the customer has
|
|
|
|
# already paid $72 for a month, -7200 = -$72 will be credited to the
|
|
|
|
# customer's balance.
|
|
|
|
stripe_customer_id = customer.stripe_customer_id
|
|
|
|
assert stripe_customer_id is not None
|
2022-01-22 06:50:24 +01:00
|
|
|
_, cb_txn = stripe.Customer.list_balance_transactions(stripe_customer_id)
|
2021-10-30 18:54:33 +02:00
|
|
|
self.assertEqual(cb_txn.amount, -7200)
|
|
|
|
self.assertEqual(
|
|
|
|
cb_txn.description,
|
|
|
|
"Credit from early termination of Standard plan",
|
|
|
|
)
|
|
|
|
self.assertEqual(cb_txn.type, "adjustment")
|
|
|
|
|
|
|
|
# The customer now only pays the difference 14400 - 7200 = 7200 = $72,
|
|
|
|
# since the unused proration is for the whole month.
|
|
|
|
(invoice,) = stripe.Invoice.list(customer=stripe_customer_id)
|
|
|
|
self.assertEqual(invoice.amount_due, 7200)
|
|
|
|
|
2020-10-14 12:17:03 +02:00
|
|
|
@mock_stripe()
|
2021-08-29 15:33:29 +02:00
|
|
|
def test_customer_has_credit_card_as_default_payment_method(self, *mocks: Mock) -> None:
|
2020-10-14 12:17:03 +02:00
|
|
|
iago = self.example_user("iago")
|
|
|
|
customer = Customer.objects.create(realm=iago.realm)
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assertFalse(customer_has_credit_card_as_default_payment_method(customer))
|
2020-10-14 12:17:03 +02:00
|
|
|
|
2023-10-26 14:11:43 +02:00
|
|
|
billable_user = RealmBillingSession(iago)
|
|
|
|
customer = billable_user.create_stripe_customer()
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assertFalse(customer_has_credit_card_as_default_payment_method(customer))
|
|
|
|
|
2023-10-26 14:11:43 +02:00
|
|
|
customer = billable_user.create_stripe_customer(
|
2021-08-29 15:33:29 +02:00
|
|
|
payment_method=create_payment_method(
|
|
|
|
self.get_test_card_number(
|
|
|
|
attaches_to_customer=True, charge_succeeds=True, card_provider="visa"
|
|
|
|
)
|
|
|
|
).id,
|
|
|
|
)
|
|
|
|
self.assertTrue(customer_has_credit_card_as_default_payment_method(customer))
|
2020-10-14 12:17:03 +02:00
|
|
|
|
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
class StripeWebhookEndpointTest(ZulipTestCase):
|
|
|
|
def test_stripe_webhook_with_invalid_data(self) -> None:
|
|
|
|
result = self.client_post(
|
|
|
|
"/stripe/webhook/",
|
|
|
|
'["dsdsds"]',
|
|
|
|
content_type="application/json",
|
|
|
|
)
|
|
|
|
self.assertEqual(result.status_code, 400)
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2021-09-07 17:53:27 +02:00
|
|
|
def test_stripe_webhook_endpoint_invalid_api_version(self) -> None:
|
|
|
|
event_data = {
|
|
|
|
"id": "stripe_event_id",
|
|
|
|
"api_version": "1991-02-20",
|
|
|
|
"type": "event_type",
|
|
|
|
"data": {"object": {"object": "checkout.session", "id": "stripe_session_id"}},
|
|
|
|
}
|
|
|
|
|
2022-02-15 23:45:41 +01:00
|
|
|
expected_error_message = rf"Mismatch between billing system Stripe API version({STRIPE_API_VERSION}) and Stripe webhook event API version(1991-02-20)."
|
2021-09-07 17:53:27 +02:00
|
|
|
with self.assertLogs("corporate.stripe", "ERROR") as error_log:
|
|
|
|
self.client_post(
|
|
|
|
"/stripe/webhook/",
|
|
|
|
event_data,
|
|
|
|
content_type="application/json",
|
|
|
|
)
|
|
|
|
self.assertEqual(error_log.output, [f"ERROR:corporate.stripe:{expected_error_message}"])
|
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
def test_stripe_webhook_for_session_completed_event(self) -> None:
|
|
|
|
valid_session_event_data = {
|
|
|
|
"id": "stripe_event_id",
|
2021-09-07 17:53:27 +02:00
|
|
|
"api_version": STRIPE_API_VERSION,
|
2021-08-29 15:33:29 +02:00
|
|
|
"type": "checkout.session.completed",
|
|
|
|
"data": {"object": {"object": "checkout.session", "id": "stripe_session_id"}},
|
|
|
|
}
|
|
|
|
with patch("corporate.views.webhook.handle_checkout_session_completed_event") as m:
|
|
|
|
result = self.client_post(
|
|
|
|
"/stripe/webhook/",
|
|
|
|
valid_session_event_data,
|
|
|
|
content_type="application/json",
|
|
|
|
)
|
|
|
|
self.assert_length(Event.objects.all(), 0)
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
m.assert_not_called()
|
|
|
|
|
|
|
|
customer = Customer.objects.create(realm=get_realm("zulip"))
|
|
|
|
Session.objects.create(
|
|
|
|
stripe_session_id="stripe_session_id",
|
|
|
|
customer=customer,
|
|
|
|
type=Session.UPGRADE_FROM_BILLING_PAGE,
|
|
|
|
)
|
|
|
|
|
|
|
|
self.assert_length(Event.objects.all(), 0)
|
|
|
|
with patch("corporate.views.webhook.handle_checkout_session_completed_event") as m:
|
|
|
|
result = self.client_post(
|
|
|
|
"/stripe/webhook/",
|
|
|
|
valid_session_event_data,
|
|
|
|
content_type="application/json",
|
|
|
|
)
|
|
|
|
[event] = Event.objects.all()
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
strip_event = stripe.Event.construct_from(valid_session_event_data, stripe.api_key)
|
|
|
|
m.assert_called_once_with(strip_event.data.object, event)
|
|
|
|
|
|
|
|
with patch("corporate.views.webhook.handle_checkout_session_completed_event") as m:
|
|
|
|
result = self.client_post(
|
|
|
|
"/stripe/webhook/",
|
|
|
|
valid_session_event_data,
|
|
|
|
content_type="application/json",
|
|
|
|
)
|
|
|
|
self.assert_length(Event.objects.all(), 1)
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
m.assert_not_called()
|
|
|
|
|
|
|
|
def test_stripe_webhook_for_payment_intent_events(self) -> None:
|
|
|
|
customer = Customer.objects.create(realm=get_realm("zulip"))
|
|
|
|
|
|
|
|
for index, event_type in enumerate(
|
|
|
|
["payment_intent.succeeded", "payment_intent.payment_failed"]
|
|
|
|
):
|
|
|
|
handler_function_name = "handle_" + event_type.replace(".", "_") + "_event"
|
|
|
|
handler_function_path = f"corporate.views.webhook.{handler_function_name}"
|
|
|
|
|
|
|
|
stripe_event_id = f"stripe_event_id_{index}"
|
|
|
|
stripe_payment_intent_id = f"stripe_payment_intent_id{index}"
|
|
|
|
|
|
|
|
valid_session_event_data = {
|
|
|
|
"id": stripe_event_id,
|
|
|
|
"type": event_type,
|
2021-09-07 17:53:27 +02:00
|
|
|
"api_version": STRIPE_API_VERSION,
|
2021-08-29 15:33:29 +02:00
|
|
|
"data": {"object": {"object": "payment_intent", "id": stripe_payment_intent_id}},
|
|
|
|
}
|
|
|
|
|
|
|
|
with patch(handler_function_path) as m:
|
|
|
|
result = self.client_post(
|
|
|
|
"/stripe/webhook/",
|
|
|
|
valid_session_event_data,
|
|
|
|
content_type="application/json",
|
|
|
|
)
|
|
|
|
self.assert_length(Event.objects.filter(stripe_event_id=stripe_event_id), 0)
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
m.assert_not_called()
|
|
|
|
|
|
|
|
PaymentIntent.objects.create(
|
|
|
|
stripe_payment_intent_id=stripe_payment_intent_id,
|
|
|
|
customer=customer,
|
|
|
|
status=PaymentIntent.REQUIRES_PAYMENT_METHOD,
|
|
|
|
)
|
|
|
|
|
|
|
|
self.assert_length(Event.objects.filter(stripe_event_id=stripe_event_id), 0)
|
|
|
|
with patch(handler_function_path) as m:
|
|
|
|
result = self.client_post(
|
|
|
|
"/stripe/webhook/",
|
|
|
|
valid_session_event_data,
|
|
|
|
content_type="application/json",
|
|
|
|
)
|
|
|
|
[event] = Event.objects.filter(stripe_event_id=stripe_event_id)
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
strip_event = stripe.Event.construct_from(valid_session_event_data, stripe.api_key)
|
|
|
|
m.assert_called_once_with(strip_event.data.object, event)
|
|
|
|
|
|
|
|
with patch(handler_function_path) as m:
|
|
|
|
result = self.client_post(
|
|
|
|
"/stripe/webhook/",
|
|
|
|
valid_session_event_data,
|
|
|
|
content_type="application/json",
|
|
|
|
)
|
|
|
|
self.assert_length(Event.objects.filter(stripe_event_id=stripe_event_id), 1)
|
|
|
|
self.assertEqual(result.status_code, 200)
|
|
|
|
m.assert_not_called()
|
|
|
|
|
|
|
|
|
|
|
|
class EventStatusTest(StripeTestCase):
|
|
|
|
def test_event_status_json_endpoint_errors(self) -> None:
|
|
|
|
self.login_user(self.example_user("iago"))
|
|
|
|
|
|
|
|
response = self.client_get("/json/billing/event/status")
|
|
|
|
self.assert_json_error_contains(response, "No customer for this organization!")
|
|
|
|
|
|
|
|
Customer.objects.create(realm=get_realm("zulip"), stripe_customer_id="cus_123")
|
|
|
|
response = self.client_get(
|
|
|
|
"/json/billing/event/status", {"stripe_session_id": "invalid_session_id"}
|
|
|
|
)
|
|
|
|
self.assert_json_error_contains(response, "Session not found")
|
|
|
|
|
|
|
|
response = self.client_get(
|
|
|
|
"/json/billing/event/status", {"stripe_payment_intent_id": "invalid_payment_intent_id"}
|
|
|
|
)
|
|
|
|
self.assert_json_error_contains(response, "Payment intent not found")
|
|
|
|
|
|
|
|
response = self.client_get(
|
|
|
|
"/json/billing/event/status",
|
|
|
|
)
|
|
|
|
self.assert_json_error_contains(
|
|
|
|
response, "Pass stripe_session_id or stripe_payment_intent_id"
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_event_status_page(self) -> None:
|
|
|
|
self.login_user(self.example_user("polonius"))
|
|
|
|
|
|
|
|
stripe_session_id = "cs_test_9QCz62mPTJQUwvhcwZHBpJMHmMZiLU512AQHU9g5znkx6NweU3j7kJvY"
|
|
|
|
response = self.client_get(
|
|
|
|
"/billing/event_status/", {"stripe_session_id": stripe_session_id}
|
|
|
|
)
|
|
|
|
self.assert_in_success_response([f'data-stripe-session-id="{stripe_session_id}"'], response)
|
|
|
|
|
|
|
|
stripe_payment_intent_id = "pi_1JGLpnA4KHR4JzRvUfkF9Tn7"
|
|
|
|
response = self.client_get(
|
|
|
|
"/billing/event_status/", {"stripe_payment_intent_id": stripe_payment_intent_id}
|
|
|
|
)
|
|
|
|
self.assert_in_success_response(
|
|
|
|
[f'data-stripe-payment-intent-id="{stripe_payment_intent_id}"'], response
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class RequiresBillingAccessTest(StripeTestCase):
|
2023-10-12 19:43:45 +02:00
|
|
|
@override
|
2021-08-29 15:33:29 +02:00
|
|
|
def setUp(self, *mocks: Mock) -> None:
|
2019-10-19 20:47:00 +02:00
|
|
|
super().setUp()
|
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-08-29 15:33:29 +02:00
|
|
|
def test_json_endpoints_permissions(self) -> None:
|
|
|
|
guest = self.example_user("polonius")
|
|
|
|
member = self.example_user("othello")
|
|
|
|
realm_admin = self.example_user("iago")
|
2020-07-14 14:40:39 +02:00
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
billing_admin = self.example_user("hamlet")
|
|
|
|
billing_admin.is_billing_admin = True
|
|
|
|
billing_admin.save(update_fields=["is_billing_admin"])
|
2020-07-14 14:40:39 +02:00
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
tested_endpoints = set()
|
|
|
|
|
|
|
|
def check_users_cant_access(
|
|
|
|
users: List[UserProfile],
|
2020-12-10 18:15:09 +01:00
|
|
|
error_message: str,
|
2021-08-29 15:33:29 +02:00
|
|
|
url: str,
|
|
|
|
method: str,
|
|
|
|
data: Dict[str, Any],
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> None:
|
2021-08-29 15:33:29 +02:00
|
|
|
tested_endpoints.add(url)
|
|
|
|
for user in users:
|
|
|
|
self.login_user(user)
|
|
|
|
if method == "POST":
|
|
|
|
client_func: Any = self.client_post
|
|
|
|
elif method == "GET":
|
|
|
|
client_func = self.client_get
|
|
|
|
else:
|
|
|
|
client_func = self.client_patch
|
|
|
|
result = client_func(
|
|
|
|
url,
|
|
|
|
data,
|
|
|
|
content_type="application/json",
|
|
|
|
)
|
|
|
|
self.assert_json_error_contains(result, error_message)
|
2020-12-10 18:15:09 +01:00
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
check_users_cant_access(
|
|
|
|
[guest],
|
|
|
|
"Must be an organization member",
|
2021-02-12 08:19:30 +01:00
|
|
|
"/json/billing/upgrade",
|
2020-12-10 18:15:09 +01:00
|
|
|
"POST",
|
2021-08-29 15:33:29 +02:00
|
|
|
{},
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2018-11-01 11:26:29 +01:00
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
check_users_cant_access(
|
|
|
|
[guest],
|
|
|
|
"Must be an organization member",
|
2021-02-12 08:19:30 +01:00
|
|
|
"/json/billing/sponsorship",
|
2020-12-10 18:15:09 +01:00
|
|
|
"POST",
|
2021-08-29 15:33:29 +02:00
|
|
|
{},
|
2021-02-12 08:19:30 +01:00
|
|
|
)
|
2020-07-15 22:18:32 +02:00
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
check_users_cant_access(
|
|
|
|
[guest, member, realm_admin],
|
|
|
|
"Must be a billing administrator or an organization owner",
|
|
|
|
"/json/billing/plan",
|
|
|
|
"PATCH",
|
|
|
|
{},
|
|
|
|
)
|
2020-07-15 22:18:32 +02:00
|
|
|
|
2021-08-29 15:33:29 +02:00
|
|
|
check_users_cant_access(
|
|
|
|
[guest, member, realm_admin],
|
|
|
|
"Must be a billing administrator or an organization owner",
|
|
|
|
"/json/billing/session/start_card_update_session",
|
|
|
|
"POST",
|
|
|
|
{},
|
|
|
|
)
|
|
|
|
|
|
|
|
check_users_cant_access(
|
|
|
|
[guest],
|
|
|
|
"Must be an organization member",
|
|
|
|
"/json/billing/session/start_retry_payment_intent_session",
|
|
|
|
"POST",
|
|
|
|
{},
|
|
|
|
)
|
|
|
|
|
|
|
|
check_users_cant_access(
|
|
|
|
[guest],
|
|
|
|
"Must be an organization member",
|
|
|
|
"/json/billing/event/status",
|
|
|
|
"GET",
|
|
|
|
{},
|
|
|
|
)
|
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-12-23 08:44:51 +01:00
|
|
|
reverse_dict = get_resolver("corporate.urls").reverse_dict
|
2021-02-12 08:19:30 +01:00
|
|
|
json_endpoints = {
|
2021-12-23 08:44:51 +01:00
|
|
|
pat
|
|
|
|
for name in reverse_dict
|
|
|
|
for matches, pat, defaults, converters in reverse_dict.getlist(name)
|
|
|
|
if pat.startswith(re.escape("json/"))
|
2021-02-12 08:19:30 +01:00
|
|
|
}
|
2021-08-29 15:33:29 +02:00
|
|
|
self.assert_length(json_endpoints, len(tested_endpoints))
|
|
|
|
|
|
|
|
@mock_stripe()
|
|
|
|
def test_billing_page_permissions(self, *mocks: Mock) -> None:
|
2023-03-22 20:05:41 +01:00
|
|
|
# Guest users can't access /upgrade/ page
|
2021-08-29 15:33:29 +02:00
|
|
|
self.login_user(self.example_user("polonius"))
|
|
|
|
response = self.client_get("/upgrade/", follow=True)
|
|
|
|
self.assertEqual(response.status_code, 404)
|
|
|
|
|
2023-03-22 20:05:41 +01:00
|
|
|
# Check that non-admins can access /upgrade/ via /billing, when there is no Customer object
|
2021-08-29 15:33:29 +02:00
|
|
|
self.login_user(self.example_user("hamlet"))
|
|
|
|
response = self.client_get("/billing/")
|
|
|
|
self.assertEqual(response.status_code, 302)
|
2022-05-29 21:12:13 +02:00
|
|
|
self.assertEqual("/upgrade/", response["Location"])
|
2021-08-29 15:33:29 +02:00
|
|
|
# Check that non-admins can sign up and pay
|
|
|
|
self.upgrade()
|
|
|
|
# Check that the non-admin hamlet can still access /billing
|
|
|
|
response = self.client_get("/billing/")
|
|
|
|
self.assert_in_success_response(["Your current plan is"], response)
|
|
|
|
|
|
|
|
# Check realm owners can access billing, even though they are not a billing admin
|
|
|
|
desdemona = self.example_user("desdemona")
|
|
|
|
desdemona.role = UserProfile.ROLE_REALM_OWNER
|
|
|
|
desdemona.save(update_fields=["role"])
|
|
|
|
self.login_user(self.example_user("desdemona"))
|
|
|
|
response = self.client_get("/billing/")
|
|
|
|
self.assert_in_success_response(["Your current plan is"], response)
|
|
|
|
|
|
|
|
# Check that member who is not a billing admin does not have access
|
|
|
|
self.login_user(self.example_user("cordelia"))
|
|
|
|
response = self.client_get("/billing/")
|
|
|
|
self.assert_in_success_response(
|
|
|
|
["You must be an organization owner or a billing administrator"], response
|
|
|
|
)
|
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-09-15 13:10:27 +02:00
|
|
|
(
|
|
|
|
(CustomerPlan.STANDARD, True, CustomerPlan.ANNUAL, None),
|
|
|
|
(anchor, month_later, year_later, 8000),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
(CustomerPlan.STANDARD, True, CustomerPlan.ANNUAL, 85),
|
|
|
|
(anchor, month_later, year_later, 1200),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
(CustomerPlan.STANDARD, True, CustomerPlan.MONTHLY, None),
|
|
|
|
(anchor, month_later, month_later, 800),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
(CustomerPlan.STANDARD, True, CustomerPlan.MONTHLY, 85),
|
|
|
|
(anchor, month_later, month_later, 120),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
(CustomerPlan.STANDARD, False, CustomerPlan.ANNUAL, None),
|
|
|
|
(anchor, year_later, year_later, 8000),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
(CustomerPlan.STANDARD, False, CustomerPlan.ANNUAL, 85),
|
|
|
|
(anchor, year_later, year_later, 1200),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
(CustomerPlan.STANDARD, False, CustomerPlan.MONTHLY, None),
|
|
|
|
(anchor, month_later, month_later, 800),
|
|
|
|
),
|
|
|
|
(
|
|
|
|
(CustomerPlan.STANDARD, 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..
|
2021-09-15 13:10:27 +02:00
|
|
|
(
|
|
|
|
(CustomerPlan.STANDARD, 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
|
2021-09-15 13:10:27 +02:00
|
|
|
(
|
|
|
|
(CustomerPlan.STANDARD, False, CustomerPlan.MONTHLY, 87.15),
|
|
|
|
(anchor, month_later, month_later, 102),
|
|
|
|
),
|
2020-06-23 06:00:56 +02:00
|
|
|
]
|
2021-02-12 08:20:45 +01:00
|
|
|
with patch("corporate.lib.stripe.timezone_now", return_value=anchor):
|
2021-09-15 13:10:27 +02:00
|
|
|
for (tier, automanage_licenses, billing_schedule, discount), output in test_cases:
|
2020-06-23 06:00:56 +02:00
|
|
|
output_ = compute_plan_parameters(
|
2021-09-15 13:10:27 +02:00
|
|
|
tier,
|
2020-06-23 06:00:56 +02:00
|
|
|
automanage_licenses,
|
2021-09-15 13:48:09 +02:00
|
|
|
billing_schedule,
|
|
|
|
None if discount is None else Decimal(discount),
|
2020-06-23 06:00:56 +02:00
|
|
|
)
|
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
|
|
|
)
|
|
|
|
|
2021-09-15 13:54:56 +02:00
|
|
|
self.assertEqual(get_price_per_license(CustomerPlan.PLUS, CustomerPlan.ANNUAL), 16000)
|
|
|
|
self.assertEqual(get_price_per_license(CustomerPlan.PLUS, CustomerPlan.MONTHLY), 1600)
|
|
|
|
self.assertEqual(
|
|
|
|
get_price_per_license(CustomerPlan.PLUS, CustomerPlan.MONTHLY, discount=Decimal(50)),
|
|
|
|
800,
|
|
|
|
)
|
2020-12-04 12:56:58 +01:00
|
|
|
|
2022-11-17 09:30:48 +01:00
|
|
|
with self.assertRaisesRegex(InvalidBillingScheduleError, "Unknown billing_schedule: 1000"):
|
2020-12-04 12:56:58 +01:00
|
|
|
get_price_per_license(CustomerPlan.STANDARD, 1000)
|
|
|
|
|
2022-11-17 09:30:48 +01:00
|
|
|
with self.assertRaisesRegex(InvalidTierError, "Unknown tier: 10"):
|
2021-09-15 13:54:56 +02:00
|
|
|
get_price_per_license(CustomerPlan.ENTERPRISE, CustomerPlan.ANNUAL)
|
|
|
|
|
2021-09-21 21:21:03 +02:00
|
|
|
def test_get_plan_renewal_or_end_date(self) -> None:
|
|
|
|
realm = get_realm("zulip")
|
|
|
|
customer = Customer.objects.create(realm=realm, stripe_customer_id="cus_12345")
|
|
|
|
billing_cycle_anchor = timezone_now()
|
|
|
|
plan = CustomerPlan.objects.create(
|
|
|
|
customer=customer,
|
|
|
|
status=CustomerPlan.ACTIVE,
|
|
|
|
billing_cycle_anchor=billing_cycle_anchor,
|
|
|
|
billing_schedule=CustomerPlan.MONTHLY,
|
|
|
|
tier=CustomerPlan.STANDARD,
|
|
|
|
)
|
|
|
|
renewal_date = get_plan_renewal_or_end_date(plan, billing_cycle_anchor)
|
|
|
|
self.assertEqual(renewal_date, add_months(billing_cycle_anchor, 1))
|
|
|
|
|
|
|
|
# When the plan ends 2 days before the start of the next billing cycle,
|
|
|
|
# the function should return the end_date.
|
|
|
|
plan_end_date = add_months(billing_cycle_anchor, 1) - timedelta(days=2)
|
|
|
|
plan.end_date = plan_end_date
|
|
|
|
plan.save(update_fields=["end_date"])
|
|
|
|
renewal_date = get_plan_renewal_or_end_date(plan, billing_cycle_anchor)
|
|
|
|
self.assertEqual(renewal_date, plan_end_date)
|
|
|
|
|
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(
|
2023-10-26 14:11:43 +02:00
|
|
|
"corporate.lib.stripe.BillingSession.create_stripe_customer", return_value="returned"
|
2021-02-12 08:19:30 +01:00
|
|
|
) as mocked1:
|
2023-10-26 14:11:43 +02:00
|
|
|
billing_session = RealmBillingSession(user)
|
|
|
|
returned = billing_session.update_or_create_stripe_customer(
|
|
|
|
payment_method="payment_method_id"
|
|
|
|
)
|
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(
|
2023-10-26 14:11:43 +02:00
|
|
|
"corporate.lib.stripe.BillingSession.create_stripe_customer", return_value="returned"
|
2021-02-12 08:19:30 +01:00
|
|
|
) as mocked2:
|
2023-10-26 14:11:43 +02:00
|
|
|
billing_session = RealmBillingSession(user)
|
|
|
|
returned = billing_session.update_or_create_stripe_customer(
|
|
|
|
payment_method="payment_method_id"
|
|
|
|
)
|
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
|
2023-10-26 14:11:43 +02:00
|
|
|
with patch("corporate.lib.stripe.BillingSession.replace_payment_method") as mocked3:
|
|
|
|
billing_session = RealmBillingSession(user)
|
|
|
|
returned_customer = billing_session.update_or_create_stripe_customer("token")
|
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
|
2023-10-26 14:11:43 +02:00
|
|
|
with patch("corporate.lib.stripe.BillingSession.replace_payment_method") as mocked4:
|
|
|
|
billing_session = RealmBillingSession(user)
|
|
|
|
returned_customer = billing_session.update_or_create_stripe_customer(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)
|
|
|
|
|
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))
|
|
|
|
|
2021-10-18 23:28:17 +02:00
|
|
|
realm.plan_type = Realm.PLAN_TYPE_STANDARD_FREE
|
2020-10-14 18:45:57 +02:00
|
|
|
realm.save()
|
|
|
|
self.assertTrue(is_sponsored_realm(realm))
|
|
|
|
|
2021-12-01 17:31:08 +01:00
|
|
|
def test_change_remote_server_plan_type(self) -> None:
|
2021-12-22 14:37:12 +01:00
|
|
|
server_uuid = str(uuid.uuid4())
|
2021-12-01 17:31:08 +01:00
|
|
|
remote_server = RemoteZulipServer.objects.create(
|
|
|
|
uuid=server_uuid,
|
|
|
|
api_key="magic_secret_api_key",
|
|
|
|
hostname="demo.example.com",
|
|
|
|
contact_email="email@example.com",
|
|
|
|
)
|
|
|
|
self.assertEqual(remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_SELF_HOSTED)
|
|
|
|
|
|
|
|
do_change_remote_server_plan_type(remote_server, RemoteZulipServer.PLAN_TYPE_STANDARD)
|
|
|
|
|
|
|
|
remote_server = RemoteZulipServer.objects.get(uuid=server_uuid)
|
|
|
|
remote_realm_audit_log = RemoteZulipServerAuditLog.objects.filter(
|
|
|
|
event_type=RealmAuditLog.REMOTE_SERVER_PLAN_TYPE_CHANGED
|
|
|
|
).last()
|
|
|
|
assert remote_realm_audit_log is not None
|
|
|
|
expected_extra_data = {
|
|
|
|
"old_value": RemoteZulipServer.PLAN_TYPE_SELF_HOSTED,
|
|
|
|
"new_value": RemoteZulipServer.PLAN_TYPE_STANDARD,
|
|
|
|
}
|
2023-07-13 19:46:06 +02:00
|
|
|
self.assertEqual(remote_realm_audit_log.extra_data, expected_extra_data)
|
2021-12-01 17:31:08 +01:00
|
|
|
self.assertEqual(remote_server.plan_type, RemoteZulipServer.PLAN_TYPE_STANDARD)
|
|
|
|
|
2021-12-15 18:53:58 +01:00
|
|
|
def test_deactivate_remote_server(self) -> None:
|
|
|
|
server_uuid = str(uuid.uuid4())
|
|
|
|
remote_server = RemoteZulipServer.objects.create(
|
|
|
|
uuid=server_uuid,
|
|
|
|
api_key="magic_secret_api_key",
|
|
|
|
hostname="demo.example.com",
|
|
|
|
contact_email="email@example.com",
|
|
|
|
)
|
|
|
|
self.assertFalse(remote_server.deactivated)
|
|
|
|
|
|
|
|
do_deactivate_remote_server(remote_server)
|
|
|
|
|
|
|
|
remote_server = RemoteZulipServer.objects.get(uuid=server_uuid)
|
|
|
|
remote_realm_audit_log = RemoteZulipServerAuditLog.objects.filter(
|
|
|
|
event_type=RealmAuditLog.REMOTE_SERVER_DEACTIVATED
|
|
|
|
).last()
|
|
|
|
assert remote_realm_audit_log is not None
|
|
|
|
self.assertTrue(remote_server.deactivated)
|
|
|
|
|
|
|
|
# Try to deactivate a remote server that is already deactivated
|
|
|
|
with self.assertLogs("corporate.stripe", "WARN") as warning_log:
|
|
|
|
do_deactivate_remote_server(remote_server)
|
|
|
|
self.assertEqual(
|
|
|
|
warning_log.output,
|
|
|
|
[
|
|
|
|
"WARNING:corporate.stripe:Cannot deactivate remote server with ID "
|
|
|
|
f"{remote_server.id}, server has already been deactivated."
|
|
|
|
],
|
|
|
|
)
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2023-10-30 14:32:02 +01:00
|
|
|
class AnalyticsHelpersTest(ZulipTestCase):
|
|
|
|
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 = Decimal(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 = Decimal(0)
|
|
|
|
zephyr_customer.save(update_fields=["default_discount"])
|
|
|
|
remote_server = RemoteZulipServer.objects.create(
|
|
|
|
uuid=str(uuid.uuid4()),
|
|
|
|
api_key="magic_secret_api_key",
|
|
|
|
hostname="demo.example.com",
|
|
|
|
contact_email="email@example.com",
|
|
|
|
)
|
|
|
|
remote_customer = Customer.objects.create(
|
|
|
|
remote_server=remote_server, stripe_customer_id="cus_4"
|
|
|
|
)
|
|
|
|
remote_customer.default_discount = Decimal(50)
|
|
|
|
remote_customer.save(update_fields=["default_discount"])
|
|
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
get_realms_with_default_discount_dict(),
|
|
|
|
{
|
|
|
|
"lear": Decimal("30.0000"),
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
|
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):
|
2021-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count + 1, False, CustomerPlan.ANNUAL, True, False)
|
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):
|
2021-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2019-01-26 02:36:37 +01:00
|
|
|
plan = CustomerPlan.objects.first()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert plan is not None
|
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):
|
2021-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count + 1, False, CustomerPlan.ANNUAL, True, False)
|
2020-12-30 18:56:57 +01:00
|
|
|
|
|
|
|
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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
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)
|
2021-07-09 02:27:06 +02:00
|
|
|
# Not a proper use of do_activate_mirror_dummy_user, but fine for this test
|
|
|
|
do_activate_mirror_dummy_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-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2019-01-28 22:57:29 +01:00
|
|
|
plan = CustomerPlan.objects.first()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert plan is not None
|
2019-01-28 22:57:29 +01:00
|
|
|
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):
|
2021-07-25 16:31:12 +02:00
|
|
|
invoice_plan(assert_is_not_none(CustomerPlan.objects.first()), self.now)
|
2019-01-28 22:57:29 +01:00
|
|
|
|
2021-06-18 21:10:45 +02:00
|
|
|
def test_invoice_plan_without_stripe_customer(self) -> None:
|
2021-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, False, False)
|
2021-06-18 21:10:45 +02:00
|
|
|
plan = get_current_plan_by_realm(get_realm("zulip"))
|
2022-06-01 01:31:28 +02:00
|
|
|
assert plan is not None
|
2021-06-18 21:10:45 +02:00
|
|
|
plan.customer.stripe_customer_id = None
|
|
|
|
plan.customer.save(update_fields=["stripe_customer_id"])
|
2021-07-04 08:19:18 +02:00
|
|
|
with self.assertRaises(BillingError) as context:
|
2021-06-18 21:10:45 +02:00
|
|
|
invoice_plan(plan, timezone_now())
|
2021-07-04 08:19:18 +02:00
|
|
|
self.assertRegex(
|
|
|
|
context.exception.error_description,
|
|
|
|
"Realm zulip has a paid plan without a Stripe customer",
|
|
|
|
)
|
2021-06-18 21:10:45 +02:00
|
|
|
|
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()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert plan is not None
|
2019-01-28 22:57:29 +01:00
|
|
|
invoice_plan(plan, self.now + timedelta(days=400))
|
2021-07-24 16:56:39 +02:00
|
|
|
stripe_cutomer_id = plan.customer.stripe_customer_id
|
|
|
|
assert stripe_cutomer_id is not None
|
|
|
|
[invoice0, invoice1] = stripe.Invoice.list(customer=stripe_cutomer_id)
|
2020-09-02 07:55:39 +02:00
|
|
|
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),
|
2022-02-05 08:29:54 +01:00
|
|
|
"description": "Zulip Cloud Standard - renewal",
|
2021-02-12 08:20:45 +01:00
|
|
|
"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
|
|
|
},
|
2023-09-12 21:10:57 +02: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()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert plan is not None
|
2019-01-28 22:57:29 +01:00
|
|
|
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)
|
2021-07-24 16:56:39 +02:00
|
|
|
stripe_customer_id = plan.customer.stripe_customer_id
|
|
|
|
assert stripe_customer_id is not None
|
|
|
|
[invoice0, invoice1] = stripe.Invoice.list(customer=stripe_customer_id)
|
2021-07-24 18:16:48 +02:00
|
|
|
self.assertEqual(invoice0.collection_method, "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,
|
2022-02-05 08:29:54 +01:00
|
|
|
"description": "Zulip Cloud Standard - renewal",
|
2021-02-12 08:20:45 +01:00
|
|
|
"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):
|
2021-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2019-01-28 22:57:29 +01:00
|
|
|
plan = CustomerPlan.objects.first()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert plan is not None
|
2019-01-28 22:57:29 +01:00
|
|
|
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
|
2021-07-24 16:56:39 +02:00
|
|
|
assert plan is not None
|
2019-01-28 22:57:29 +01:00
|
|
|
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):
|
2021-08-29 15:33:29 +02:00
|
|
|
self.local_upgrade(self.seat_count, True, CustomerPlan.ANNUAL, True, False)
|
2019-01-28 22:57:29 +01:00
|
|
|
plan = CustomerPlan.objects.first()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert plan is not None
|
2019-01-28 22:57:29 +01:00
|
|
|
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()
|
2021-07-24 16:56:39 +02:00
|
|
|
assert plan is not None
|
2019-01-28 22:57:29 +01:00
|
|
|
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()
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD)
|
2021-06-03 12:20:31 +02:00
|
|
|
|
|
|
|
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()
|
2021-10-18 23:28:17 +02:00
|
|
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD)
|
2023-10-31 19:22:55 +01:00
|
|
|
|
|
|
|
|
2023-11-02 17:44:02 +01:00
|
|
|
class TestRealmBillingSession(StripeTestCase):
|
|
|
|
def test_get_audit_log_error(self) -> None:
|
|
|
|
user = self.example_user("hamlet")
|
|
|
|
billing_session = RealmBillingSession(user)
|
|
|
|
fake_audit_log = typing.cast(AuditLogEventType, 0)
|
|
|
|
with self.assertRaisesRegex(
|
|
|
|
BillingSessionAuditLogEventError, "Unknown audit log event type: 0"
|
|
|
|
):
|
|
|
|
billing_session.get_audit_log_event(event_type=fake_audit_log)
|
|
|
|
|
|
|
|
|
2023-10-31 19:22:55 +01:00
|
|
|
class TestSupportBillingHelpers(StripeTestCase):
|
|
|
|
def test_get_discount_for_realm(self) -> None:
|
|
|
|
iago = self.example_user("iago")
|
|
|
|
user = self.example_user("hamlet")
|
|
|
|
self.assertEqual(get_discount_for_realm(user.realm), None)
|
|
|
|
|
|
|
|
attach_discount_to_realm(user.realm, Decimal(85), acting_user=iago)
|
|
|
|
self.assertEqual(get_discount_for_realm(user.realm), 85)
|
|
|
|
|
|
|
|
@mock_stripe()
|
|
|
|
def test_attach_discount_to_realm(self, *mocks: Mock) -> None:
|
|
|
|
# Attach discount before Stripe customer exists
|
|
|
|
support_admin = self.example_user("iago")
|
|
|
|
user = self.example_user("hamlet")
|
|
|
|
attach_discount_to_realm(user.realm, Decimal(85), acting_user=support_admin)
|
|
|
|
realm_audit_log = RealmAuditLog.objects.filter(
|
|
|
|
event_type=RealmAuditLog.REALM_DISCOUNT_CHANGED
|
|
|
|
).last()
|
|
|
|
assert realm_audit_log is not None
|
|
|
|
expected_extra_data = {"old_discount": None, "new_discount": str(Decimal("85"))}
|
|
|
|
self.assertEqual(realm_audit_log.extra_data, expected_extra_data)
|
|
|
|
self.login_user(user)
|
|
|
|
# Check that the discount appears in page_params
|
|
|
|
self.assert_in_success_response(["85"], self.client_get("/upgrade/"))
|
|
|
|
# Check that the customer was charged the discounted amount
|
|
|
|
self.upgrade()
|
|
|
|
customer = Customer.objects.first()
|
|
|
|
assert customer is not None
|
|
|
|
[charge] = stripe.Charge.list(customer=customer.stripe_customer_id)
|
|
|
|
self.assertEqual(1200 * self.seat_count, charge.amount)
|
|
|
|
stripe_customer_id = customer.stripe_customer_id
|
|
|
|
assert stripe_customer_id is not None
|
|
|
|
[invoice] = stripe.Invoice.list(customer=stripe_customer_id)
|
|
|
|
self.assertEqual(
|
|
|
|
[1200 * self.seat_count, -1200 * self.seat_count],
|
|
|
|
[item.amount for item in invoice.lines],
|
|
|
|
)
|
|
|
|
# Check CustomerPlan reflects the discount
|
|
|
|
plan = CustomerPlan.objects.get(price_per_license=1200, discount=Decimal(85))
|
|
|
|
|
|
|
|
# Attach discount to existing Stripe customer
|
|
|
|
plan.status = CustomerPlan.ENDED
|
|
|
|
plan.save(update_fields=["status"])
|
|
|
|
attach_discount_to_realm(user.realm, Decimal(25), acting_user=support_admin)
|
|
|
|
with patch("corporate.lib.stripe.timezone_now", return_value=self.now):
|
|
|
|
self.upgrade(license_management="automatic", billing_modality="charge_automatically")
|
|
|
|
[charge, _] = stripe.Charge.list(customer=customer.stripe_customer_id)
|
|
|
|
self.assertEqual(6000 * self.seat_count, charge.amount)
|
|
|
|
stripe_customer_id = customer.stripe_customer_id
|
|
|
|
assert stripe_customer_id is not None
|
|
|
|
[invoice, _] = stripe.Invoice.list(customer=stripe_customer_id)
|
|
|
|
self.assertEqual(
|
|
|
|
[6000 * self.seat_count, -6000 * self.seat_count],
|
|
|
|
[item.amount for item in invoice.lines],
|
|
|
|
)
|
|
|
|
plan = CustomerPlan.objects.get(price_per_license=6000, discount=Decimal(25))
|
|
|
|
|
|
|
|
attach_discount_to_realm(user.realm, Decimal(50), acting_user=support_admin)
|
|
|
|
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))
|
|
|
|
stripe_customer_id = customer.stripe_customer_id
|
|
|
|
assert stripe_customer_id is not None
|
|
|
|
[invoice, _, _] = stripe.Invoice.list(customer=stripe_customer_id)
|
|
|
|
self.assertEqual([4000 * self.seat_count], [item.amount for item in invoice.lines])
|
|
|
|
realm_audit_log = RealmAuditLog.objects.filter(
|
|
|
|
event_type=RealmAuditLog.REALM_DISCOUNT_CHANGED
|
|
|
|
).last()
|
|
|
|
assert realm_audit_log is not None
|
|
|
|
expected_extra_data = {
|
|
|
|
"old_discount": str(Decimal("25.0000")),
|
|
|
|
"new_discount": str(Decimal("50")),
|
|
|
|
}
|
|
|
|
self.assertEqual(realm_audit_log.extra_data, expected_extra_data)
|
|
|
|
self.assertEqual(realm_audit_log.acting_user, support_admin)
|
2023-11-02 15:23:35 +01:00
|
|
|
|
|
|
|
def test_approve_realm_sponsorship(self) -> None:
|
|
|
|
realm = get_realm("zulip")
|
|
|
|
self.assertNotEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD_FREE)
|
|
|
|
|
|
|
|
support_admin = self.example_user("iago")
|
|
|
|
approve_realm_sponsorship(realm, acting_user=support_admin)
|
|
|
|
self.assertEqual(realm.plan_type, Realm.PLAN_TYPE_STANDARD_FREE)
|
|
|
|
|
|
|
|
expected_message = (
|
|
|
|
"Your organization's request for sponsored hosting has been approved! You have been upgraded to Zulip Cloud Standard, free of charge. :tada:"
|
|
|
|
"\n\nIf you could [list Zulip as a sponsor on your website](/help/linking-to-zulip-website), we would really appreciate it!"
|
|
|
|
)
|
|
|
|
sender = get_system_bot(settings.NOTIFICATION_BOT, realm.id)
|
|
|
|
recipient_id = self.example_user("desdemona").recipient_id
|
|
|
|
message = Message.objects.filter(realm_id=realm.id, sender=sender.id).first()
|
|
|
|
assert message is not None
|
|
|
|
self.assertEqual(message.content, expected_message)
|
|
|
|
self.assertEqual(message.recipient.type, Recipient.PERSONAL)
|
|
|
|
self.assertEqual(message.recipient_id, recipient_id)
|
2023-11-02 18:17:08 +01:00
|
|
|
|
|
|
|
def test_update_realm_sponsorship_status(self) -> None:
|
|
|
|
lear = get_realm("lear")
|
|
|
|
iago = self.example_user("iago")
|
|
|
|
update_realm_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()
|
|
|
|
assert realm_audit_log is not None
|
|
|
|
expected_extra_data = {"sponsorship_pending": True}
|
|
|
|
self.assertEqual(realm_audit_log.extra_data, expected_extra_data)
|
|
|
|
self.assertEqual(realm_audit_log.acting_user, iago)
|
2023-11-02 18:42:04 +01:00
|
|
|
|
|
|
|
def test_update_realm_billing_method(self) -> None:
|
|
|
|
realm = get_realm("zulip")
|
|
|
|
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.assertEqual(plan.charge_automatically, False)
|
|
|
|
|
|
|
|
iago = self.example_user("iago")
|
|
|
|
update_realm_billing_method(realm, True, acting_user=iago)
|
|
|
|
plan.refresh_from_db()
|
|
|
|
self.assertEqual(plan.charge_automatically, True)
|
|
|
|
realm_audit_log = RealmAuditLog.objects.filter(
|
|
|
|
event_type=RealmAuditLog.REALM_BILLING_METHOD_CHANGED
|
|
|
|
).last()
|
|
|
|
assert realm_audit_log is not None
|
|
|
|
expected_extra_data = {"charge_automatically": plan.charge_automatically}
|
|
|
|
self.assertEqual(realm_audit_log.acting_user, iago)
|
|
|
|
self.assertEqual(realm_audit_log.extra_data, expected_extra_data)
|
|
|
|
|
|
|
|
update_realm_billing_method(realm, False, acting_user=iago)
|
|
|
|
plan.refresh_from_db()
|
|
|
|
self.assertEqual(plan.charge_automatically, False)
|
|
|
|
realm_audit_log = RealmAuditLog.objects.filter(
|
|
|
|
event_type=RealmAuditLog.REALM_BILLING_METHOD_CHANGED
|
|
|
|
).last()
|
|
|
|
assert realm_audit_log is not None
|
|
|
|
expected_extra_data = {"charge_automatically": plan.charge_automatically}
|
|
|
|
self.assertEqual(realm_audit_log.acting_user, iago)
|
|
|
|
self.assertEqual(realm_audit_log.extra_data, expected_extra_data)
|