@@ -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