mirror of https://github.com/zulip/zulip.git
billing: Update and unify billing error handling.
This commit is contained in:
parent
79dddd5b80
commit
5719633992
|
@ -64,38 +64,34 @@ def unsign_string(signed_string: str, salt: str) -> str:
|
|||
signer = Signer(salt=salt)
|
||||
return signer.unsign(signed_string)
|
||||
|
||||
class StripeError(JsonableError):
|
||||
pass
|
||||
|
||||
class BillingError(Exception):
|
||||
pass
|
||||
# error messages
|
||||
CONTACT_SUPPORT = _("Something went wrong. Please contact %s)" % (settings.ZULIP_ADMINISTRATOR,))
|
||||
TRY_RELOADING = _("Something went wrong. Please reload the page.")
|
||||
|
||||
# description is used only for tests
|
||||
def __init__(self, description: str, message: str) -> None:
|
||||
self.description = description
|
||||
self.message = message
|
||||
|
||||
def catch_stripe_errors(func: CallableT) -> CallableT:
|
||||
@wraps(func)
|
||||
def wrapped(*args: Any, **kwargs: Any) -> Any:
|
||||
if settings.DEVELOPMENT and not settings.TEST_SUITE: # nocoverage
|
||||
if STRIPE_PUBLISHABLE_KEY is None:
|
||||
raise AssertionError(
|
||||
"Missing Stripe config. "
|
||||
"See https://zulip.readthedocs.io/en/latest/subsystems/billing.html.")
|
||||
raise BillingError('missing stripe config', "Missing Stripe config. "
|
||||
"See https://zulip.readthedocs.io/en/latest/subsystems/billing.html.")
|
||||
if not Plan.objects.exists():
|
||||
raise AssertionError(
|
||||
"Plan objects not created. Please run ./manage.py setup_stripe")
|
||||
|
||||
raise BillingError('missing plans',
|
||||
"Plan objects not created. Please run ./manage.py setup_stripe")
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except stripe.error.StripeError as e:
|
||||
billing_logger.error("Stripe error: %d %s",
|
||||
e.http_status, e.__class__.__name__)
|
||||
billing_logger.error("Stripe error: %d %s", e.http_status, e.__class__.__name__)
|
||||
if isinstance(e, stripe.error.CardError):
|
||||
raise StripeError(e.json_body.get('error', {}).get('message'))
|
||||
raise BillingError('card error', e.json_body.get('error', {}).get('message'))
|
||||
else:
|
||||
raise StripeError(
|
||||
_("Something went wrong. Please try again or email us at %s.")
|
||||
% (settings.ZULIP_ADMINISTRATOR,))
|
||||
except Exception:
|
||||
billing_logger.exception("Uncaught error in Stripe integration")
|
||||
raise
|
||||
raise BillingError('other stripe error', BillingError.CONTACT_SUPPORT)
|
||||
return wrapped # type: ignore # https://github.com/python/mypy/issues/1927
|
||||
|
||||
@catch_stripe_errors
|
||||
|
@ -147,10 +143,11 @@ def do_create_customer_with_payment_source(user: UserProfile, stripe_token: str)
|
|||
def do_subscribe_customer_to_plan(stripe_customer: stripe.Customer, stripe_plan_id: str,
|
||||
seat_count: int, tax_percent: float) -> None:
|
||||
if extract_current_subscription(stripe_customer) is not None:
|
||||
# Most likely due to a race condition where two people in the org
|
||||
# try to upgrade their plan at the same time
|
||||
billing_logger.error("Stripe customer %s trying to subscribe to %s, "
|
||||
"but has an active subscription" % (stripe_customer.id, stripe_plan_id))
|
||||
# TODO: Change to an error sent to the frontend
|
||||
raise BillingError("Your organization has an existing active subscription.")
|
||||
raise BillingError('subscribing with existing subscription', BillingError.TRY_RELOADING)
|
||||
stripe_subscription = stripe.Subscription.create(
|
||||
customer=stripe_customer.id,
|
||||
billing='charge_automatically',
|
||||
|
@ -187,13 +184,13 @@ def process_initial_upgrade(user: UserProfile, plan: str, signed_seat_count: str
|
|||
if plan not in [Plan.CLOUD_ANNUAL, Plan.CLOUD_MONTHLY]:
|
||||
billing_logger.warning("Tampered plan during realm upgrade. user: %s, realm: %s (%s)."
|
||||
% (user.id, user.realm.id, user.realm.string_id))
|
||||
raise BillingError("Something went wrong. Please contact support@zulipchat.com")
|
||||
raise BillingError('tampered plan', BillingError.CONTACT_SUPPORT)
|
||||
try:
|
||||
seat_count = int(unsign_string(signed_seat_count, salt))
|
||||
except signing.BadSignature:
|
||||
billing_logger.warning("Tampered seat count during realm upgrade. user: %s, realm: %s (%s)."
|
||||
% (user.id, user.realm.id, user.realm.string_id))
|
||||
raise BillingError("Something went wrong. Please contact support@zulipchat.com")
|
||||
raise BillingError('tampered seat count', BillingError.CONTACT_SUPPORT)
|
||||
|
||||
stripe_customer = do_create_customer_with_payment_source(user, stripe_token)
|
||||
do_subscribe_customer_to_plan(
|
||||
|
|
|
@ -12,7 +12,7 @@ from zerver.lib.actions import do_deactivate_user, do_create_user, \
|
|||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.lib.timestamp import timestamp_to_datetime
|
||||
from zerver.models import Realm, UserProfile, get_realm, RealmAuditLog
|
||||
from zilencer.lib.stripe import StripeError, catch_stripe_errors, \
|
||||
from zilencer.lib.stripe import catch_stripe_errors, \
|
||||
do_create_customer_with_payment_source, do_subscribe_customer_to_plan, \
|
||||
get_seat_count, extract_current_subscription, sign_string, unsign_string, \
|
||||
BillingError
|
||||
|
@ -62,10 +62,10 @@ class StripeTest(ZulipTestCase):
|
|||
def test_errors(self, mock_billing_logger_error: mock.Mock) -> None:
|
||||
@catch_stripe_errors
|
||||
def raise_invalid_request_error() -> None:
|
||||
raise stripe.error.InvalidRequestError("Request req_oJU621i6H6X4Ez: No such token: x",
|
||||
None)
|
||||
with self.assertRaisesRegex(StripeError, "Something went wrong. Please try again or "):
|
||||
raise stripe.error.InvalidRequestError("Request req_oJU621i6H6X4Ez: No such token: x", None)
|
||||
with self.assertRaises(BillingError) as context:
|
||||
raise_invalid_request_error()
|
||||
self.assertEqual('other stripe error', context.exception.description)
|
||||
mock_billing_logger_error.assert_called()
|
||||
|
||||
@catch_stripe_errors
|
||||
|
@ -74,16 +74,10 @@ class StripeTest(ZulipTestCase):
|
|||
json_body = {"error": {"message": error_message}}
|
||||
raise stripe.error.CardError(error_message, "number", "invalid_number",
|
||||
json_body=json_body)
|
||||
with self.assertRaisesRegex(StripeError,
|
||||
"The card number is not a valid credit card number."):
|
||||
with self.assertRaises(BillingError) as context:
|
||||
raise_card_error()
|
||||
mock_billing_logger_error.assert_called()
|
||||
|
||||
@catch_stripe_errors
|
||||
def raise_exception() -> None:
|
||||
raise Exception
|
||||
with self.assertRaises(Exception):
|
||||
raise_exception()
|
||||
self.assertIn('not a valid credit card', context.exception.message)
|
||||
self.assertEqual('card error', context.exception.description)
|
||||
mock_billing_logger_error.assert_called()
|
||||
|
||||
@mock.patch("stripe.Customer.create", side_effect=mock_create_customer)
|
||||
|
@ -204,23 +198,25 @@ class StripeTest(ZulipTestCase):
|
|||
|
||||
def test_upgrade_with_tampered_seat_count(self) -> None:
|
||||
self.login(self.example_email("hamlet"))
|
||||
result = self.client_post("/upgrade/", {
|
||||
response = self.client_post("/upgrade/", {
|
||||
'stripeToken': self.token,
|
||||
'signed_seat_count': "randomsalt",
|
||||
'salt': self.salt,
|
||||
'plan': Plan.CLOUD_ANNUAL
|
||||
})
|
||||
self.assert_in_success_response(["Something went wrong. Please contact"], result)
|
||||
self.assert_in_success_response(["Upgrade to Zulip Premium"], response)
|
||||
self.assertEqual(response['error_description'], 'tampered seat count')
|
||||
|
||||
def test_upgrade_with_tampered_plan(self) -> None:
|
||||
self.login(self.example_email("hamlet"))
|
||||
result = self.client_post("/upgrade/", {
|
||||
response = self.client_post("/upgrade/", {
|
||||
'stripeToken': self.token,
|
||||
'signed_seat_count': self.signed_seat_count,
|
||||
'salt': self.salt,
|
||||
'plan': "invalid"
|
||||
})
|
||||
self.assert_in_success_response(["Something went wrong. Please contact"], result)
|
||||
self.assert_in_success_response(["Upgrade to Zulip Premium"], response)
|
||||
self.assertEqual(response['error_description'], 'tampered plan')
|
||||
|
||||
@mock.patch("stripe.Customer.retrieve", side_effect=mock_customer_with_subscription)
|
||||
@mock.patch("stripe.Invoice.upcoming", side_effect=mock_upcoming_invoice)
|
||||
|
@ -264,8 +260,10 @@ class StripeTest(ZulipTestCase):
|
|||
self.assertIsNone(extract_current_subscription(mock_customer_with_canceled_subscription()))
|
||||
|
||||
def test_subscribe_customer_to_second_plan(self) -> None:
|
||||
with self.assertRaisesRegex(BillingError, "Your organization has an existing active subscription."):
|
||||
do_subscribe_customer_to_plan(mock_customer_with_subscription(), self.stripe_plan_id, self.quantity, 0)
|
||||
with self.assertRaises(BillingError) as context:
|
||||
do_subscribe_customer_to_plan(mock_customer_with_subscription(),
|
||||
self.stripe_plan_id, self.quantity, 0)
|
||||
self.assertEqual(context.exception.description, 'subscribing with existing subscription')
|
||||
|
||||
def test_sign_string(self) -> None:
|
||||
string = "abc"
|
||||
|
|
|
@ -24,7 +24,7 @@ from zerver.lib.validator import check_int, check_string, check_url, \
|
|||
from zerver.lib.timestamp import timestamp_to_datetime
|
||||
from zerver.models import UserProfile, Realm
|
||||
from zerver.views.push_notifications import validate_token
|
||||
from zilencer.lib.stripe import STRIPE_PUBLISHABLE_KEY, StripeError, \
|
||||
from zilencer.lib.stripe import STRIPE_PUBLISHABLE_KEY, \
|
||||
get_stripe_customer, get_upcoming_invoice, get_seat_count, \
|
||||
extract_current_subscription, process_initial_upgrade, sign_string, \
|
||||
BillingError
|
||||
|
@ -165,6 +165,7 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
|||
|
||||
user = request.user
|
||||
error_message = ""
|
||||
error_description = "" # only used in tests
|
||||
|
||||
if Customer.objects.filter(realm=user.realm).exists():
|
||||
return HttpResponseRedirect(reverse('zilencer.views.billing_home'))
|
||||
|
@ -173,10 +174,12 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
|||
try:
|
||||
process_initial_upgrade(user, request.POST['plan'], request.POST['signed_seat_count'],
|
||||
request.POST['salt'], request.POST['stripeToken'])
|
||||
except (BillingError, StripeError) as e:
|
||||
error_message = str(e)
|
||||
except Exception:
|
||||
error_message = "Something went wrong. Please contact support@zulipchat.com."
|
||||
except BillingError as e:
|
||||
error_message = e.message
|
||||
error_description = e.description
|
||||
except Exception as e:
|
||||
billing_logger.exception("Uncaught exception in billing: %s" % (e,))
|
||||
error_message = BillingError.CONTACT_SUPPORT
|
||||
else:
|
||||
return HttpResponseRedirect(reverse('zilencer.views.billing_home'))
|
||||
|
||||
|
@ -193,7 +196,9 @@ def initial_upgrade(request: HttpRequest) -> HttpResponse:
|
|||
'nickname_annual': Plan.CLOUD_ANNUAL,
|
||||
'error_message': error_message,
|
||||
} # type: Dict[str, Any]
|
||||
return render(request, 'zilencer/upgrade.html', context=context)
|
||||
response = render(request, 'zilencer/upgrade.html', context=context)
|
||||
response['error_description'] = error_description
|
||||
return response
|
||||
|
||||
PLAN_NAMES = {
|
||||
Plan.CLOUD_ANNUAL: "Zulip Premium (billed annually)",
|
||||
|
|
Loading…
Reference in New Issue