mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-30 19:43:47 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			182 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			182 lines
		
	
	
		
			6.6 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import logging
 | |
| from typing import Any, Callable, Dict, Union
 | |
| 
 | |
| import stripe
 | |
| from django.conf import settings
 | |
| 
 | |
| from corporate.lib.stripe import (
 | |
|     BillingError,
 | |
|     UpgradeWithExistingPlanError,
 | |
|     ensure_realm_does_not_have_active_plan,
 | |
|     process_initial_upgrade,
 | |
|     update_or_create_stripe_customer,
 | |
| )
 | |
| from corporate.models import 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)
 | |
|     payment_method = stripe_setup_intent.payment_method
 | |
| 
 | |
|     if session.type in [
 | |
|         Session.UPGRADE_FROM_BILLING_PAGE,
 | |
|         Session.RETRY_UPGRADE_WITH_ANOTHER_PAYMENT_METHOD,
 | |
|     ]:
 | |
|         ensure_realm_does_not_have_active_plan(user.realm)
 | |
|         update_or_create_stripe_customer(user, payment_method)
 | |
|         session.payment_intent.status = PaymentIntent.PROCESSING
 | |
|         session.payment_intent.last_payment_error = ()
 | |
|         session.payment_intent.save(update_fields=["status", "last_payment_error"])
 | |
|         try:
 | |
|             stripe.PaymentIntent.confirm(
 | |
|                 session.payment_intent.stripe_payment_intent_id,
 | |
|                 payment_method=payment_method,
 | |
|                 off_session=True,
 | |
|             )
 | |
|         except stripe.error.CardError:
 | |
|             pass
 | |
|     elif session.type in [
 | |
|         Session.FREE_TRIAL_UPGRADE_FROM_BILLING_PAGE,
 | |
|         Session.FREE_TRIAL_UPGRADE_FROM_ONBOARDING_PAGE,
 | |
|     ]:
 | |
|         ensure_realm_does_not_have_active_plan(user.realm)
 | |
|         update_or_create_stripe_customer(user, payment_method)
 | |
|         process_initial_upgrade(
 | |
|             user,
 | |
|             int(stripe_setup_intent.metadata["licenses"]),
 | |
|             stripe_setup_intent.metadata["license_management"] == "automatic",
 | |
|             int(stripe_setup_intent.metadata["billing_schedule"]),
 | |
|             charge_automatically=True,
 | |
|             free_trial=True,
 | |
|         )
 | |
|     elif session.type in [Session.CARD_UPDATE_FROM_BILLING_PAGE]:
 | |
|         update_or_create_stripe_customer(user, 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_realm_does_not_have_active_plan(user.realm)
 | |
|     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="Zulip Cloud Standard Credit",
 | |
|         )
 | |
|         stripe.Invoice.finalize_invoice(stripe_invoice)
 | |
|         raise e
 | |
| 
 | |
|     process_initial_upgrade(
 | |
|         user,
 | |
|         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: Event
 | |
| ) -> None:
 | |
|     payment_intent.status = PaymentIntent.get_status_integer_from_status_text(
 | |
|         stripe_payment_intent.status
 | |
|     )
 | |
|     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"])
 |