stripe: Move error handling into stripe.py too.

This completes the separation of our logic for managing Stripe
customers from the view code for the billing page.

As we add more features to our Customer model and to our Stripe
integration, we might further separate those two things; but for now
they're nearly synonymous and there's no problem in them being mixed
together.
This commit is contained in:
Greg Price 2018-01-30 12:03:59 -08:00
parent 0b81762350
commit f9b12952f8
2 changed files with 41 additions and 22 deletions

View File

@ -1,12 +1,13 @@
from functools import wraps
import logging import logging
import os import os
from typing import Any, Callable, TypeVar
from django.conf import settings from django.conf import settings
from django.utils.translation import ugettext as _
import stripe import stripe
from stripe.error import CardError, RateLimitError, InvalidRequestError, \
AuthenticationError, APIConnectionError, StripeError
from zerver.lib.exceptions import JsonableError
from zerver.lib.logging_util import log_to_file from zerver.lib.logging_util import log_to_file
from zerver.models import Realm, UserProfile from zerver.models import Realm, UserProfile
from zilencer.models import Customer from zilencer.models import Customer
@ -24,6 +25,35 @@ billing_logger = logging.getLogger('zilencer.stripe')
log_to_file(billing_logger, BILLING_LOG_PATH) log_to_file(billing_logger, BILLING_LOG_PATH)
log_to_file(logging.getLogger('stripe'), BILLING_LOG_PATH) log_to_file(logging.getLogger('stripe'), BILLING_LOG_PATH)
CallableT = TypeVar('CallableT', bound=Callable[..., Any])
class StripeError(JsonableError):
pass
def catch_stripe_errors(func: CallableT) -> CallableT:
@wraps(func)
def wrapped(*args: Any, **kwargs: Any) -> Any:
if STRIPE_PUBLISHABLE_KEY is None:
# Dev-only message; no translation needed.
raise StripeError(
"Missing Stripe config. In dev, add to zproject/dev-secrets.conf .")
try:
return func(*args, **kwargs)
except stripe.error.StripeError as e:
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'))
else:
raise StripeError(
_("Something went wrong. Please try again or email us at %s.")
% (settings.ZULIP_ADMINISTRATOR,))
except Exception as e:
billing_logger.exception("Uncaught error in Stripe integration")
raise
return wrapped # type: ignore # https://github.com/python/mypy/issues/1927
@catch_stripe_errors
def count_stripe_cards(realm: Realm) -> int: def count_stripe_cards(realm: Realm) -> int:
try: try:
customer_obj = Customer.objects.get(realm=realm) customer_obj = Customer.objects.get(realm=realm)
@ -32,6 +62,7 @@ def count_stripe_cards(realm: Realm) -> int:
except Customer.DoesNotExist: except Customer.DoesNotExist:
return 0 return 0
@catch_stripe_errors
def save_stripe_token(user: UserProfile, token: str) -> int: def save_stripe_token(user: UserProfile, token: str) -> int:
"""Returns total number of cards.""" """Returns total number of cards."""
# The card metadata doesn't show up in Dashboard but can be accessed # The card metadata doesn't show up in Dashboard but can be accessed

View File

@ -19,7 +19,7 @@ from zerver.lib.validator import check_int
from zerver.models import UserProfile, Realm from zerver.models import UserProfile, Realm
from zerver.views.push_notifications import validate_token from zerver.views.push_notifications import validate_token
from zilencer.lib.stripe import STRIPE_PUBLISHABLE_KEY, count_stripe_cards, \ from zilencer.lib.stripe import STRIPE_PUBLISHABLE_KEY, count_stripe_cards, \
save_stripe_token, CardError, StripeError, billing_logger save_stripe_token, StripeError
from zilencer.models import RemotePushDeviceToken, RemoteZulipServer from zilencer.models import RemotePushDeviceToken, RemoteZulipServer
def validate_entity(entity: Union[UserProfile, RemoteZulipServer]) -> None: def validate_entity(entity: Union[UserProfile, RemoteZulipServer]) -> None:
@ -112,16 +112,11 @@ def add_payment_method(request: HttpRequest) -> HttpResponse:
"email": user.email, "email": user.email,
} # type: Dict[str, Any] } # type: Dict[str, Any]
def render_error(message: str) -> HttpResponse:
ctx["error_message"] = message
return render(request, 'zilencer/billing.html', context=ctx)
if not user.is_realm_admin: if not user.is_realm_admin:
return render_error(_("You should be an administrator of the organization %s to view this page.") ctx["error_message"] = (
_("You should be an administrator of the organization %s to view this page.")
% (user.realm.name,)) % (user.realm.name,))
if STRIPE_PUBLISHABLE_KEY is None: return render(request, 'zilencer/billing.html', context=ctx)
# Dev-only message; no translation needed.
return render_error("Missing Stripe config. In dev, add to zproject/dev-secrets.conf .")
try: try:
if request.method == "GET": if request.method == "GET":
@ -133,12 +128,5 @@ def add_payment_method(request: HttpRequest) -> HttpResponse:
ctx["payment_method_added"] = True ctx["payment_method_added"] = True
return render(request, 'zilencer/billing.html', context=ctx) return render(request, 'zilencer/billing.html', context=ctx)
except StripeError as e: except StripeError as e:
billing_logger.error("Stripe error: %d %s", e.http_status, e.__class__.__name__) ctx["error_message"] = e.msg
if isinstance(e, CardError): return render(request, 'zilencer/billing.html', context=ctx)
return render_error(e.json_body.get('error', {}).get('message'))
else:
return render_error(_("Something went wrong. Please try again or email us at %s.")
% (settings.ZULIP_ADMINISTRATOR,))
except Exception as e:
billing_logger.exception("Uncaught error in billing")
raise