Skip to content

Commit

Permalink
cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
suejung-sentry committed Jan 24, 2025
1 parent 48b2e89 commit 27940a4
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 189 deletions.
219 changes: 104 additions & 115 deletions billing/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,10 @@ def _log_updated(self, updated: List[Owner]) -> None:

def invoice_payment_succeeded(self, invoice: stripe.Invoice) -> None:
"""
Stripe invoice.payment_succeeded is called when an invoice is paid. This happens
when an initial checkout session is completed (first upgrade from free to paid) or
upon a recurring schedule for the subscription (e.g., monthly or annually)
Stripe invoice.payment_succeeded webhook event is emitted when an invoice is paid.
This happens when an initial checkout session (first upgrade from free to paid)
is completed as the subscription initially "charges_automatically" or upon the
recurring schedule for the subscription (monthly or annually)
"""
log.info(
"Invoice Payment Succeeded - Setting delinquency status False",
Expand Down Expand Up @@ -90,35 +91,27 @@ def invoice_payment_succeeded(self, invoice: stripe.Invoice) -> None:

def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
"""
Stripe invoice.payment_failed is called when an invoice is not paid. This happens
when a recurring schedule for the subscription (e.g., monthly or annually) fails to pay.
Or when the initial checkout session fails to pay.
Stripe invoice.payment_failed webhook event is emitted when an invoice payment fails
(initial or recurring). Note that delayed payment methods (including ACH with
microdeposits) may have a failed initial invoice until the account is verified
if that is the only payment method for the customer.
"""
if invoice.status == "open":
if invoice.default_payment_method is None:
# check if customer has any pending payment methods
unverified_payment_methods = get_unverified_payment_methods(
self, invoice.customer
)
if unverified_payment_methods:
# Skip if this is from an initial checkout session with an incomplete payment_intent
# (e.g. due to ACH requiring async microdeposits verification)
if invoice.default_payment_method is None:
if invoice.payment_intent:
payment_intent = stripe.PaymentIntent.retrieve(invoice.payment_intent)
if payment_intent.status == "requires_action":
log.info(
"Invoice payment failed but customer has pending payment methods",
"Invoice payment failed but still awaiting known customer action, skipping Delinquency actions",
extra=dict(
stripe_customer_id=invoice.customer,
stripe_subscription_id=invoice.subscription,
pending_payment_methods=len(unverified_payment_methods),
payment_intent_status=payment_intent.status,
next_action=payment_intent.next_action,
),
)
return
# reach here because ach is still pending
log.info(
"Invoice payment failed but requires action - skipping delinquency",
extra=dict(
stripe_customer_id=invoice.customer,
stripe_subscription_id=invoice.subscription,
),
)
return

log.info(
"Invoice Payment Failed - Setting Delinquency status True",
Expand Down Expand Up @@ -176,9 +169,21 @@ def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:

def customer_subscription_deleted(self, subscription: stripe.Subscription) -> None:
"""
Stripe customer.subscription.deleted is called when a subscription is deleted.
This happens when an org goes from paid to free.
Stripe customer.subscription.deleted webhook event is emitted when a subscription is deleted.
This happens when an org goes from paid to free (see payment_service.delete_subscription)
or when cleaning up an incomplete subscription that never activated (e.g., abandoned async
ACH microdeposits verification).
"""
if subscription.status == "incomplete":
log.info(
"Customer Subscription Deleted - Ignoring incomplete subscription",
extra=dict(
stripe_subscription_id=subscription.id,
stripe_customer_id=subscription.customer,
),
)
return

log.info(
"Customer Subscription Deleted - Setting free plan and deactivating repos for stripe customer",
extra=dict(
Expand Down Expand Up @@ -224,7 +229,6 @@ def subscription_schedule_created(
),
)

# handler for Stripe event subscription_schedule.updated
def subscription_schedule_updated(
self, schedule: stripe.SubscriptionSchedule
) -> None:
Expand All @@ -249,7 +253,6 @@ def subscription_schedule_updated(
),
)

# handler for Stripe event subscription_schedule.released
def subscription_schedule_released(
self, schedule: stripe.SubscriptionSchedule
) -> None:
Expand Down Expand Up @@ -290,7 +293,7 @@ def subscription_schedule_released(

def customer_created(self, customer: stripe.Customer) -> None:
"""
Stripe customer.created is called when a customer is created.
Stripe customer.created webhook event is emitted when a customer is created.
This happens when an owner completes a CheckoutSession for the first time.
"""
# Based on what stripe doesn't gives us (an ownerid!)
Expand All @@ -299,8 +302,11 @@ def customer_created(self, customer: stripe.Customer) -> None:
# relying on customer.subscription.created to handle sub creation
log.info("Customer created", extra=dict(stripe_customer_id=customer.id))

# handler for Stripe event customer.subscription.created
def customer_subscription_created(self, subscription: stripe.Subscription) -> None:
"""
Stripe customer.subscription.created webhook event is emitted when a subscription is created.
This happens when an owner completes a CheckoutSession for the first time.
"""
log.info(
"Customer subscription created",
extra=dict(
Expand Down Expand Up @@ -349,24 +355,15 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No
owner.stripe_customer_id = subscription.customer
owner.save()

# check if the subscription has a pending_update attribute, if so, don't upgrade the plan yet
print("subscription what are you", subscription)
# Check if subscription has a default payment method
has_default_payment = subscription.default_payment_method is not None

# If no default payment, check for any pending verification methods
if not has_default_payment:
payment_methods = get_unverified_payment_methods(subscription.customer)
if payment_methods:
log.info(
"Subscription has pending payment verification",
extra=dict(
subscription_id=subscription.id,
customer_id=subscription.customer,
payment_methods=payment_methods,
),
)
return
if self._has_unverified_initial_payment_method(subscription):
log.info(
"Subscription has pending initial payment verification - will upgrade plan after initial invoice payment",
extra=dict(
subscription_id=subscription.id,
customer_id=subscription.customer,
),
)
return

plan_service = PlanService(current_org=owner)
plan_service.expire_trial_when_upgrading()
Expand All @@ -385,8 +382,31 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No

self._log_updated([owner])

# handler for Stripe event customer.subscription.updated
def _has_unverified_initial_payment_method(
self, subscription: stripe.Subscription
) -> bool:
"""
Helper method to check if a subscription's latest invoice has a payment intent
that requires verification (e.g. ACH microdeposits)
"""
latest_invoice = stripe.Invoice.retrieve(subscription.latest_invoice)
if latest_invoice and latest_invoice.payment_intent:
payment_intent = stripe.PaymentIntent.retrieve(
latest_invoice.payment_intent
)
return (
payment_intent is not None
and payment_intent.status == "requires_action"
)
return False

def customer_subscription_updated(self, subscription: stripe.Subscription) -> None:
"""
Stripe customer.subscription.updated webhook event is emitted when a subscription is updated.
This happens throughout the Stripe subscription lifecycle; the times we care about are:
- when an owner updates the subscription's default payment method using our update_payment_method api
- ... (TODO: what else?)
"""
log.info(
"Customer subscription updated",
extra=dict(
Expand All @@ -409,24 +429,15 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
)
return

# check if the subscription has a pending_update attribute, if so, don't upgrade the plan yet
print("subscription what are you", subscription)
# Check if subscription has a default payment method
has_default_payment = subscription.default_payment_method is not None

# If no default payment, check for any pending verification methods
if not has_default_payment:
payment_methods = get_unverified_payment_methods(subscription.customer)
if payment_methods:
log.info(
"Subscription has pending payment verification",
extra=dict(
subscription_id=subscription.id,
customer_id=subscription.customer,
payment_methods=payment_methods,
),
)
return
if self._has_unverified_initial_payment_method(subscription):
log.info(
"Subscription has pending initial payment verification - will upgrade plan after initial invoice payment",
extra=dict(
subscription_id=subscription.id,
customer_id=subscription.customer,
),
)
return

indication_of_payment_failure = getattr(subscription, "pending_update", None)
if indication_of_payment_failure:
Expand All @@ -442,6 +453,7 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
),
)
return

# Properly attach the payment method on the customer
# This hook will be called after a checkout session completes,
# updating the subscription created with it
Expand Down Expand Up @@ -507,7 +519,6 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
),
)

# handler for Stripe event customer.updated
def customer_updated(self, customer: stripe.Customer) -> None:
new_default_payment_method = customer["invoice_settings"][
"default_payment_method"
Expand All @@ -529,7 +540,6 @@ def customer_updated(self, customer: stripe.Customer) -> None:
subscription["id"], default_payment_method=new_default_payment_method
)

# handler for Stripe event checkout.session.completed
def checkout_session_completed(
self, checkout_session: stripe.checkout.Session
) -> None:
Expand All @@ -550,12 +560,21 @@ def checkout_session_completed(
def _check_and_handle_delayed_notification_payment_methods(
self, customer_id: str, payment_method_id: str
):
"""
Helper method to handle payment methods that require delayed verification (like ACH).
When verification succeeds, this attaches the payment method to the customer and sets
it as the default payment method for both the customer and subscription.
"""
owner = Owner.objects.get(stripe_customer_id=customer_id)
payment_method = stripe.PaymentMethod.retrieve(payment_method_id)

if payment_method.type == "us_bank_account" and hasattr(
is_us_bank_account = payment_method.type == "us_bank_account" and hasattr(
payment_method, "us_bank_account"
):
)

should_set_as_default = is_us_bank_account

if should_set_as_default:
# attach the payment method + set as default on the invoice and subscription
stripe.PaymentMethod.attach(
payment_method, customer=owner.stripe_customer_id
Expand All @@ -570,13 +589,16 @@ def _check_and_handle_delayed_notification_payment_methods(

def payment_intent_succeeded(self, payment_intent: stripe.PaymentIntent) -> None:
"""
Stripe payment intent is used for the initial checkout session.
Success is emitted when the payment intent goes to a success state.
Stripe payment_intent.succeeded webhook event is emitted when a
payment intent goes to a success state.
We create a Stripe PaymentIntent for the initial checkout session.
"""
log.info(
"Payment intent succeeded",
extra=dict(
payment_method_id=payment_intent.id,
stripe_customer_id=payment_intent.customer,
payment_intent_id=payment_intent.id,
payment_method_type=payment_intent.payment_method,
),
)

Expand All @@ -586,12 +608,17 @@ def payment_intent_succeeded(self, payment_intent: stripe.PaymentIntent) -> None

def setup_intent_succeeded(self, setup_intent: stripe.SetupIntent) -> None:
"""
Stripe setup intent is used for subsequent edits to payment methods.
See our createSetupIntent api which is called from the UI Stripe Payment Element
Stripe setup_intent.succeeded webhook event is emitted when a setup intent
goes to a success state. We create a Stripe SetupIntent for the gazebo UI
PaymentElement to modify payment methods.
"""
log.info(
"Setup intent succeeded",
extra=dict(setup_intent_id=setup_intent.id),
extra=dict(
stripe_customer_id=setup_intent.customer,
setup_intent_id=setup_intent.id,
payment_method_type=setup_intent.payment_method,
),
)

self._check_and_handle_delayed_notification_payment_methods(
Expand Down Expand Up @@ -629,41 +656,3 @@ def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Response:
getattr(self, self.event.type.replace(".", "_"))(self.event.data.object)

return Response(status=status.HTTP_204_NO_CONTENT)


# TODO - move this
def get_unverified_payment_methods(self, stripe_customer_id: str):

unverified_payment_methods = []

# Check payment intents
payment_intents = stripe.PaymentIntent.list(customer=stripe_customer_id, limit=100)
for intent in payment_intents.data:
if (
hasattr(intent, "next_action")
and intent.next_action
and intent.next_action.type == "verify_with_microdeposits"
):
unverified_payment_methods.append(
{
"payment_method_id": intent.payment_method,
"hosted_verification_link": intent.next_action.verify_with_microdeposits.hosted_verification_url,
}
)

# Check setup intents
setup_intents = stripe.SetupIntent.list(customer=stripe_customer_id, limit=100)
for intent in setup_intents.data:
if (
hasattr(intent, "next_action")
and intent.next_action
and intent.next_action.type == "verify_with_microdeposits"
):
unverified_payment_methods.append(
{
"payment_method_id": intent.payment_method,
"hosted_verification_link": intent.next_action.verify_with_microdeposits.hosted_verification_url,
}
)

return unverified_payment_methods
Loading

0 comments on commit 27940a4

Please sign in to comment.