Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit 27940a4

Browse files
cleanup
1 parent 48b2e89 commit 27940a4

File tree

2 files changed

+181
-189
lines changed

2 files changed

+181
-189
lines changed

billing/views.py

Lines changed: 104 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,10 @@ def _log_updated(self, updated: List[Owner]) -> None:
3838

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

9192
def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
9293
"""
93-
Stripe invoice.payment_failed is called when an invoice is not paid. This happens
94-
when a recurring schedule for the subscription (e.g., monthly or annually) fails to pay.
95-
Or when the initial checkout session fails to pay.
94+
Stripe invoice.payment_failed webhook event is emitted when an invoice payment fails
95+
(initial or recurring). Note that delayed payment methods (including ACH with
96+
microdeposits) may have a failed initial invoice until the account is verified
97+
if that is the only payment method for the customer.
9698
"""
97-
if invoice.status == "open":
98-
if invoice.default_payment_method is None:
99-
# check if customer has any pending payment methods
100-
unverified_payment_methods = get_unverified_payment_methods(
101-
self, invoice.customer
102-
)
103-
if unverified_payment_methods:
99+
# Skip if this is from an initial checkout session with an incomplete payment_intent
100+
# (e.g. due to ACH requiring async microdeposits verification)
101+
if invoice.default_payment_method is None:
102+
if invoice.payment_intent:
103+
payment_intent = stripe.PaymentIntent.retrieve(invoice.payment_intent)
104+
if payment_intent.status == "requires_action":
104105
log.info(
105-
"Invoice payment failed but customer has pending payment methods",
106+
"Invoice payment failed but still awaiting known customer action, skipping Delinquency actions",
106107
extra=dict(
107108
stripe_customer_id=invoice.customer,
108109
stripe_subscription_id=invoice.subscription,
109-
pending_payment_methods=len(unverified_payment_methods),
110+
payment_intent_status=payment_intent.status,
111+
next_action=payment_intent.next_action,
110112
),
111113
)
112114
return
113-
# reach here because ach is still pending
114-
log.info(
115-
"Invoice payment failed but requires action - skipping delinquency",
116-
extra=dict(
117-
stripe_customer_id=invoice.customer,
118-
stripe_subscription_id=invoice.subscription,
119-
),
120-
)
121-
return
122115

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

177170
def customer_subscription_deleted(self, subscription: stripe.Subscription) -> None:
178171
"""
179-
Stripe customer.subscription.deleted is called when a subscription is deleted.
180-
This happens when an org goes from paid to free.
172+
Stripe customer.subscription.deleted webhook event is emitted when a subscription is deleted.
173+
This happens when an org goes from paid to free (see payment_service.delete_subscription)
174+
or when cleaning up an incomplete subscription that never activated (e.g., abandoned async
175+
ACH microdeposits verification).
181176
"""
177+
if subscription.status == "incomplete":
178+
log.info(
179+
"Customer Subscription Deleted - Ignoring incomplete subscription",
180+
extra=dict(
181+
stripe_subscription_id=subscription.id,
182+
stripe_customer_id=subscription.customer,
183+
),
184+
)
185+
return
186+
182187
log.info(
183188
"Customer Subscription Deleted - Setting free plan and deactivating repos for stripe customer",
184189
extra=dict(
@@ -224,7 +229,6 @@ def subscription_schedule_created(
224229
),
225230
)
226231

227-
# handler for Stripe event subscription_schedule.updated
228232
def subscription_schedule_updated(
229233
self, schedule: stripe.SubscriptionSchedule
230234
) -> None:
@@ -249,7 +253,6 @@ def subscription_schedule_updated(
249253
),
250254
)
251255

252-
# handler for Stripe event subscription_schedule.released
253256
def subscription_schedule_released(
254257
self, schedule: stripe.SubscriptionSchedule
255258
) -> None:
@@ -290,7 +293,7 @@ def subscription_schedule_released(
290293

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

302-
# handler for Stripe event customer.subscription.created
303305
def customer_subscription_created(self, subscription: stripe.Subscription) -> None:
306+
"""
307+
Stripe customer.subscription.created webhook event is emitted when a subscription is created.
308+
This happens when an owner completes a CheckoutSession for the first time.
309+
"""
304310
log.info(
305311
"Customer subscription created",
306312
extra=dict(
@@ -349,24 +355,15 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No
349355
owner.stripe_customer_id = subscription.customer
350356
owner.save()
351357

352-
# check if the subscription has a pending_update attribute, if so, don't upgrade the plan yet
353-
print("subscription what are you", subscription)
354-
# Check if subscription has a default payment method
355-
has_default_payment = subscription.default_payment_method is not None
356-
357-
# If no default payment, check for any pending verification methods
358-
if not has_default_payment:
359-
payment_methods = get_unverified_payment_methods(subscription.customer)
360-
if payment_methods:
361-
log.info(
362-
"Subscription has pending payment verification",
363-
extra=dict(
364-
subscription_id=subscription.id,
365-
customer_id=subscription.customer,
366-
payment_methods=payment_methods,
367-
),
368-
)
369-
return
358+
if self._has_unverified_initial_payment_method(subscription):
359+
log.info(
360+
"Subscription has pending initial payment verification - will upgrade plan after initial invoice payment",
361+
extra=dict(
362+
subscription_id=subscription.id,
363+
customer_id=subscription.customer,
364+
),
365+
)
366+
return
370367

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

386383
self._log_updated([owner])
387384

388-
# handler for Stripe event customer.subscription.updated
385+
def _has_unverified_initial_payment_method(
386+
self, subscription: stripe.Subscription
387+
) -> bool:
388+
"""
389+
Helper method to check if a subscription's latest invoice has a payment intent
390+
that requires verification (e.g. ACH microdeposits)
391+
"""
392+
latest_invoice = stripe.Invoice.retrieve(subscription.latest_invoice)
393+
if latest_invoice and latest_invoice.payment_intent:
394+
payment_intent = stripe.PaymentIntent.retrieve(
395+
latest_invoice.payment_intent
396+
)
397+
return (
398+
payment_intent is not None
399+
and payment_intent.status == "requires_action"
400+
)
401+
return False
402+
389403
def customer_subscription_updated(self, subscription: stripe.Subscription) -> None:
404+
"""
405+
Stripe customer.subscription.updated webhook event is emitted when a subscription is updated.
406+
This happens throughout the Stripe subscription lifecycle; the times we care about are:
407+
- when an owner updates the subscription's default payment method using our update_payment_method api
408+
- ... (TODO: what else?)
409+
"""
390410
log.info(
391411
"Customer subscription updated",
392412
extra=dict(
@@ -409,24 +429,15 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
409429
)
410430
return
411431

412-
# check if the subscription has a pending_update attribute, if so, don't upgrade the plan yet
413-
print("subscription what are you", subscription)
414-
# Check if subscription has a default payment method
415-
has_default_payment = subscription.default_payment_method is not None
416-
417-
# If no default payment, check for any pending verification methods
418-
if not has_default_payment:
419-
payment_methods = get_unverified_payment_methods(subscription.customer)
420-
if payment_methods:
421-
log.info(
422-
"Subscription has pending payment verification",
423-
extra=dict(
424-
subscription_id=subscription.id,
425-
customer_id=subscription.customer,
426-
payment_methods=payment_methods,
427-
),
428-
)
429-
return
432+
if self._has_unverified_initial_payment_method(subscription):
433+
log.info(
434+
"Subscription has pending initial payment verification - will upgrade plan after initial invoice payment",
435+
extra=dict(
436+
subscription_id=subscription.id,
437+
customer_id=subscription.customer,
438+
),
439+
)
440+
return
430441

431442
indication_of_payment_failure = getattr(subscription, "pending_update", None)
432443
if indication_of_payment_failure:
@@ -442,6 +453,7 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
442453
),
443454
)
444455
return
456+
445457
# Properly attach the payment method on the customer
446458
# This hook will be called after a checkout session completes,
447459
# updating the subscription created with it
@@ -507,7 +519,6 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
507519
),
508520
)
509521

510-
# handler for Stripe event customer.updated
511522
def customer_updated(self, customer: stripe.Customer) -> None:
512523
new_default_payment_method = customer["invoice_settings"][
513524
"default_payment_method"
@@ -529,7 +540,6 @@ def customer_updated(self, customer: stripe.Customer) -> None:
529540
subscription["id"], default_payment_method=new_default_payment_method
530541
)
531542

532-
# handler for Stripe event checkout.session.completed
533543
def checkout_session_completed(
534544
self, checkout_session: stripe.checkout.Session
535545
) -> None:
@@ -550,12 +560,21 @@ def checkout_session_completed(
550560
def _check_and_handle_delayed_notification_payment_methods(
551561
self, customer_id: str, payment_method_id: str
552562
):
563+
"""
564+
Helper method to handle payment methods that require delayed verification (like ACH).
565+
When verification succeeds, this attaches the payment method to the customer and sets
566+
it as the default payment method for both the customer and subscription.
567+
"""
553568
owner = Owner.objects.get(stripe_customer_id=customer_id)
554569
payment_method = stripe.PaymentMethod.retrieve(payment_method_id)
555570

556-
if payment_method.type == "us_bank_account" and hasattr(
571+
is_us_bank_account = payment_method.type == "us_bank_account" and hasattr(
557572
payment_method, "us_bank_account"
558-
):
573+
)
574+
575+
should_set_as_default = is_us_bank_account
576+
577+
if should_set_as_default:
559578
# attach the payment method + set as default on the invoice and subscription
560579
stripe.PaymentMethod.attach(
561580
payment_method, customer=owner.stripe_customer_id
@@ -570,13 +589,16 @@ def _check_and_handle_delayed_notification_payment_methods(
570589

571590
def payment_intent_succeeded(self, payment_intent: stripe.PaymentIntent) -> None:
572591
"""
573-
Stripe payment intent is used for the initial checkout session.
574-
Success is emitted when the payment intent goes to a success state.
592+
Stripe payment_intent.succeeded webhook event is emitted when a
593+
payment intent goes to a success state.
594+
We create a Stripe PaymentIntent for the initial checkout session.
575595
"""
576596
log.info(
577597
"Payment intent succeeded",
578598
extra=dict(
579-
payment_method_id=payment_intent.id,
599+
stripe_customer_id=payment_intent.customer,
600+
payment_intent_id=payment_intent.id,
601+
payment_method_type=payment_intent.payment_method,
580602
),
581603
)
582604

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

587609
def setup_intent_succeeded(self, setup_intent: stripe.SetupIntent) -> None:
588610
"""
589-
Stripe setup intent is used for subsequent edits to payment methods.
590-
See our createSetupIntent api which is called from the UI Stripe Payment Element
611+
Stripe setup_intent.succeeded webhook event is emitted when a setup intent
612+
goes to a success state. We create a Stripe SetupIntent for the gazebo UI
613+
PaymentElement to modify payment methods.
591614
"""
592615
log.info(
593616
"Setup intent succeeded",
594-
extra=dict(setup_intent_id=setup_intent.id),
617+
extra=dict(
618+
stripe_customer_id=setup_intent.customer,
619+
setup_intent_id=setup_intent.id,
620+
payment_method_type=setup_intent.payment_method,
621+
),
595622
)
596623

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

631658
return Response(status=status.HTTP_204_NO_CONTENT)
632-
633-
634-
# TODO - move this
635-
def get_unverified_payment_methods(self, stripe_customer_id: str):
636-
637-
unverified_payment_methods = []
638-
639-
# Check payment intents
640-
payment_intents = stripe.PaymentIntent.list(customer=stripe_customer_id, limit=100)
641-
for intent in payment_intents.data:
642-
if (
643-
hasattr(intent, "next_action")
644-
and intent.next_action
645-
and intent.next_action.type == "verify_with_microdeposits"
646-
):
647-
unverified_payment_methods.append(
648-
{
649-
"payment_method_id": intent.payment_method,
650-
"hosted_verification_link": intent.next_action.verify_with_microdeposits.hosted_verification_url,
651-
}
652-
)
653-
654-
# Check setup intents
655-
setup_intents = stripe.SetupIntent.list(customer=stripe_customer_id, limit=100)
656-
for intent in setup_intents.data:
657-
if (
658-
hasattr(intent, "next_action")
659-
and intent.next_action
660-
and intent.next_action.type == "verify_with_microdeposits"
661-
):
662-
unverified_payment_methods.append(
663-
{
664-
"payment_method_id": intent.payment_method,
665-
"hosted_verification_link": intent.next_action.verify_with_microdeposits.hosted_verification_url,
666-
}
667-
)
668-
669-
return unverified_payment_methods

0 commit comments

Comments
 (0)