mirror of
https://github.com/zulip/zulip.git
synced 2025-11-08 16:01:58 +00:00
Updates `process_initial_upgrade` to take a plan_tier parameter, so that information can be specific to the type of BillingSession. Note that ideally the plan tier would be passed as metadata to the stripe.checkout.Session, but in order to do so, we need to be able to update the generated stripe fixtures for tests. So for now, we set the plan tier directly in the stripe event handler code.
186 lines
6.8 KiB
Python
186 lines
6.8 KiB
Python
import logging
|
|
from contextlib import suppress
|
|
from typing import Any, Callable, Dict, Union
|
|
|
|
import stripe
|
|
from django.conf import settings
|
|
|
|
from corporate.lib.stripe import (
|
|
BillingError,
|
|
RealmBillingSession,
|
|
UpgradeWithExistingPlanError,
|
|
ensure_customer_does_not_have_active_plan,
|
|
process_initial_upgrade,
|
|
)
|
|
from corporate.models import CustomerPlan, Event, PaymentIntent, Session
|
|
from zerver.models import get_active_user_profile_by_id_in_realm
|
|
|
|
billing_logger = logging.getLogger("corporate.stripe")
|
|
|
|
|
|
def error_handler(
|
|
func: Callable[[Any, Any], None],
|
|
) -> Callable[[Union[stripe.checkout.Session, stripe.PaymentIntent], Event], None]:
|
|
def wrapper(
|
|
stripe_object: Union[stripe.checkout.Session, stripe.PaymentIntent], event: Event
|
|
) -> None:
|
|
event.status = Event.EVENT_HANDLER_STARTED
|
|
event.save(update_fields=["status"])
|
|
|
|
try:
|
|
func(stripe_object, event.content_object)
|
|
except BillingError as e:
|
|
billing_logger.warning(
|
|
"BillingError in %s event handler: %s. stripe_object_id=%s, customer_id=%s metadata=%s",
|
|
event.type,
|
|
e.error_description,
|
|
stripe_object.id,
|
|
stripe_object.customer,
|
|
stripe_object.metadata,
|
|
)
|
|
event.status = Event.EVENT_HANDLER_FAILED
|
|
event.handler_error = {
|
|
"message": e.msg,
|
|
"description": e.error_description,
|
|
}
|
|
event.save(update_fields=["status", "handler_error"])
|
|
except Exception:
|
|
billing_logger.exception(
|
|
"Uncaught exception in %s event handler:",
|
|
event.type,
|
|
stack_info=True,
|
|
)
|
|
event.status = Event.EVENT_HANDLER_FAILED
|
|
event.handler_error = {
|
|
"description": f"uncaught exception in {event.type} event handler",
|
|
"message": BillingError.CONTACT_SUPPORT.format(email=settings.ZULIP_ADMINISTRATOR),
|
|
}
|
|
event.save(update_fields=["status", "handler_error"])
|
|
else:
|
|
event.status = Event.EVENT_HANDLER_SUCCEEDED
|
|
event.save()
|
|
|
|
return wrapper
|
|
|
|
|
|
@error_handler
|
|
def handle_checkout_session_completed_event(
|
|
stripe_session: stripe.checkout.Session, session: Session
|
|
) -> None:
|
|
session.status = Session.COMPLETED
|
|
session.save()
|
|
|
|
stripe_setup_intent = stripe.SetupIntent.retrieve(stripe_session.setup_intent)
|
|
assert session.customer.realm is not None
|
|
user_id = stripe_session.metadata.get("user_id")
|
|
assert user_id is not None
|
|
user = get_active_user_profile_by_id_in_realm(user_id, session.customer.realm)
|
|
billing_session = RealmBillingSession(user)
|
|
payment_method = stripe_setup_intent.payment_method
|
|
|
|
if session.type in [
|
|
Session.UPGRADE_FROM_BILLING_PAGE,
|
|
Session.RETRY_UPGRADE_WITH_ANOTHER_PAYMENT_METHOD,
|
|
]:
|
|
ensure_customer_does_not_have_active_plan(session.customer)
|
|
billing_session.update_or_create_stripe_customer(payment_method)
|
|
assert session.payment_intent is not None
|
|
session.payment_intent.status = PaymentIntent.PROCESSING
|
|
session.payment_intent.last_payment_error = ()
|
|
session.payment_intent.save(update_fields=["status", "last_payment_error"])
|
|
with suppress(stripe.error.CardError):
|
|
stripe.PaymentIntent.confirm(
|
|
session.payment_intent.stripe_payment_intent_id,
|
|
payment_method=payment_method,
|
|
off_session=True,
|
|
)
|
|
elif session.type in [
|
|
Session.FREE_TRIAL_UPGRADE_FROM_BILLING_PAGE,
|
|
Session.FREE_TRIAL_UPGRADE_FROM_ONBOARDING_PAGE,
|
|
]:
|
|
ensure_customer_does_not_have_active_plan(session.customer)
|
|
billing_session.update_or_create_stripe_customer(payment_method)
|
|
process_initial_upgrade(
|
|
user,
|
|
CustomerPlan.STANDARD,
|
|
int(stripe_session.metadata["licenses"]),
|
|
stripe_session.metadata["license_management"] == "automatic",
|
|
int(stripe_session.metadata["billing_schedule"]),
|
|
charge_automatically=True,
|
|
free_trial=True,
|
|
)
|
|
elif session.type in [Session.CARD_UPDATE_FROM_BILLING_PAGE]:
|
|
billing_session.update_or_create_stripe_customer(payment_method)
|
|
|
|
|
|
@error_handler
|
|
def handle_payment_intent_succeeded_event(
|
|
stripe_payment_intent: stripe.PaymentIntent, payment_intent: PaymentIntent
|
|
) -> None:
|
|
payment_intent.status = PaymentIntent.SUCCEEDED
|
|
payment_intent.save()
|
|
metadata: Dict[str, Any] = stripe_payment_intent.metadata
|
|
assert payment_intent.customer.realm is not None
|
|
user_id = metadata.get("user_id")
|
|
assert user_id is not None
|
|
user = get_active_user_profile_by_id_in_realm(user_id, payment_intent.customer.realm)
|
|
|
|
description = ""
|
|
for charge in stripe_payment_intent.charges:
|
|
description = f"Payment (Card ending in {charge.payment_method_details.card.last4})"
|
|
break
|
|
|
|
stripe.InvoiceItem.create(
|
|
amount=stripe_payment_intent.amount * -1,
|
|
currency="usd",
|
|
customer=stripe_payment_intent.customer,
|
|
description=description,
|
|
discountable=False,
|
|
)
|
|
try:
|
|
ensure_customer_does_not_have_active_plan(payment_intent.customer)
|
|
except UpgradeWithExistingPlanError as e:
|
|
stripe_invoice = stripe.Invoice.create(
|
|
auto_advance=True,
|
|
collection_method="charge_automatically",
|
|
customer=stripe_payment_intent.customer,
|
|
days_until_due=None,
|
|
statement_descriptor="Cloud Standard Credit",
|
|
)
|
|
stripe.Invoice.finalize_invoice(stripe_invoice)
|
|
raise e
|
|
|
|
process_initial_upgrade(
|
|
user,
|
|
CustomerPlan.STANDARD,
|
|
int(metadata["licenses"]),
|
|
metadata["license_management"] == "automatic",
|
|
int(metadata["billing_schedule"]),
|
|
True,
|
|
False,
|
|
)
|
|
|
|
|
|
@error_handler
|
|
def handle_payment_intent_payment_failed_event(
|
|
stripe_payment_intent: stripe.PaymentIntent, payment_intent: PaymentIntent
|
|
) -> None:
|
|
payment_intent.status = PaymentIntent.get_status_integer_from_status_text(
|
|
stripe_payment_intent.status
|
|
)
|
|
assert payment_intent.customer.realm is not None
|
|
billing_logger.info(
|
|
"Stripe payment intent failed: %s %s %s %s",
|
|
payment_intent.customer.realm.string_id,
|
|
stripe_payment_intent.last_payment_error.get("type"),
|
|
stripe_payment_intent.last_payment_error.get("code"),
|
|
stripe_payment_intent.last_payment_error.get("param"),
|
|
)
|
|
payment_intent.last_payment_error = {
|
|
"description": stripe_payment_intent.last_payment_error.get("type"),
|
|
}
|
|
payment_intent.last_payment_error["message"] = stripe_payment_intent.last_payment_error.get(
|
|
"message"
|
|
)
|
|
payment_intent.save(update_fields=["status", "last_payment_error"])
|