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 d163c61
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 206 deletions.
228 changes: 95 additions & 133 deletions billing/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,6 @@ 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)
"""
log.info(
"Invoice Payment Succeeded - Setting delinquency status False",
extra=dict(
Expand Down Expand Up @@ -90,35 +85,24 @@ 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 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:
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(

Check warning on line 96 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L96

Added line #L96 was not covered by tests
"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

Check warning on line 105 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L105

Added line #L105 was not covered by tests
# 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 +160,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(

Check warning on line 169 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L169

Added line #L169 was not covered by tests
"Customer Subscription Deleted - Ignoring incomplete subscription",
extra=dict(
stripe_subscription_id=subscription.id,
stripe_customer_id=subscription.customer,
),
)
return

Check warning on line 176 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L176

Added line #L176 was not covered by tests

log.info(
"Customer Subscription Deleted - Setting free plan and deactivating repos for stripe customer",
extra=dict(
Expand Down Expand Up @@ -224,7 +220,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 +244,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 @@ -289,24 +283,17 @@ def subscription_schedule_released(
)

def customer_created(self, customer: stripe.Customer) -> None:
"""
Stripe customer.created is called 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!)
# in this event we cannot reliably create a customer,
# so we're just logging that we created the event and
# 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:
log.info(
"Customer subscription created",
extra=dict(
customer_id=subscription["customer"], subscription_id=subscription["id"]
),
)
"""
Stripe customer.subscription.created webhook event is emitted when a subscription is created.
This happens when an owner completes a CheckoutSession for a new subscription.
"""
sub_item_plan_id = subscription.plan.id

if not sub_item_plan_id:
Expand Down Expand Up @@ -349,24 +336,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(

Check warning on line 340 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L340

Added line #L340 was not covered by tests
"Subscription has pending initial payment verification - will upgrade plan after initial invoice payment",
extra=dict(
subscription_id=subscription.id,
customer_id=subscription.customer,
),
)
return

Check warning on line 347 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L347

Added line #L347 was not covered by tests

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

self._log_updated([owner])

# handler for Stripe event customer.subscription.updated
def customer_subscription_updated(self, subscription: stripe.Subscription) -> None:
log.info(
"Customer subscription updated",
extra=dict(
customer_id=subscription["customer"], subscription_id=subscription["id"]
),
)
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(

Check warning on line 375 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L374-L375

Added lines #L374 - L375 were not covered by tests
latest_invoice.payment_intent
)
return (

Check warning on line 378 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L378

Added line #L378 was not covered by tests
payment_intent is not None
and payment_intent.status == "requires_action"
)
return False

Check warning on line 382 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L382

Added line #L382 was not covered by tests

def customer_subscription_updated(self, subscription: stripe.Subscription) -> None:
"""
Stripe customer.subscription.updated webhook event is emitted when a subscription is updated.
This can happen when an owner updates the subscription's default payment method using our
update_payment_method api
"""
owners: QuerySet[Owner] = Owner.objects.filter(
stripe_subscription_id=subscription.id,
stripe_customer_id=subscription.customer,
Expand All @@ -409,24 +402,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(

Check warning on line 406 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L406

Added line #L406 was not covered by tests
"Subscription has pending initial payment verification - will upgrade plan after initial invoice payment",
extra=dict(
subscription_id=subscription.id,
customer_id=subscription.customer,
),
)
return

Check warning on line 413 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L413

Added line #L413 was not covered by tests

indication_of_payment_failure = getattr(subscription, "pending_update", None)
if indication_of_payment_failure:
Expand All @@ -442,6 +426,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 +492,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 +513,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 +533,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)

Check warning on line 542 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L541-L542

Added lines #L541 - L542 were not covered by tests

if payment_method.type == "us_bank_account" and hasattr(
is_us_bank_account = payment_method.type == "us_bank_account" and hasattr(

Check warning on line 544 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L544

Added line #L544 was not covered by tests
payment_method, "us_bank_account"
):
)

should_set_as_default = is_us_bank_account

Check warning on line 548 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L548

Added line #L548 was not covered by tests

if should_set_as_default:

Check warning on line 550 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L550

Added line #L550 was not covered by tests
# attach the payment method + set as default on the invoice and subscription
stripe.PaymentMethod.attach(

Check warning on line 552 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L552

Added line #L552 was not covered by tests
payment_method, customer=owner.stripe_customer_id
Expand All @@ -570,13 +562,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(

Check warning on line 569 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L569

Added line #L569 was not covered by tests
"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 +581,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(

Check warning on line 588 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L588

Added line #L588 was not covered by tests
"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(

Check warning on line 597 in billing/views.py

View check run for this annotation

Codecov Notifications / codecov/patch

billing/views.py#L597

Added line #L597 was not covered by tests
Expand Down Expand Up @@ -629,41 +629,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 d163c61

Please sign in to comment.