1414
1515from billing .helpers import get_all_admins_for_owners
1616from codecov_auth .models import Owner
17+ from services .billing import BillingService
1718from services .task .task import TaskService
1819
1920from .constants import StripeHTTPHeaders , StripeWebhookEvents
@@ -36,6 +37,11 @@ def _log_updated(self, updated: List[Owner]) -> None:
3637 )
3738
3839 def invoice_payment_succeeded (self , invoice : stripe .Invoice ) -> None :
40+ """
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)
44+ """
3945 log .info (
4046 "Invoice Payment Succeeded - Setting delinquency status False" ,
4147 extra = dict (
@@ -82,7 +88,39 @@ def invoice_payment_succeeded(self, invoice: stripe.Invoice) -> None:
8288 ** template_vars ,
8389 )
8490
91+ # handler for Stripe event invoice.payment_failed
8592 def invoice_payment_failed (self , invoice : stripe .Invoice ) -> None :
93+ """
94+ Stripe invoice.payment_failed is called when an invoice is not paid. This happens
95+ when a recurring schedule for the subscription (e.g., monthly or annually) fails to pay.
96+ Or when the initial checkout session fails to pay.
97+ """
98+ if invoice .status == "open" :
99+ if invoice .default_payment_method is None :
100+ # check if customer has any pending payment methods
101+ unverified_payment_methods = get_unverified_payment_methods (
102+ self , invoice .customer
103+ )
104+ if unverified_payment_methods :
105+ log .info (
106+ "Invoice payment failed but customer has pending payment methods" ,
107+ extra = dict (
108+ stripe_customer_id = invoice .customer ,
109+ stripe_subscription_id = invoice .subscription ,
110+ pending_payment_methods = len (unverified_payment_methods ),
111+ ),
112+ )
113+ return
114+ # reach here because ach is still pending
115+ log .info (
116+ "Invoice payment failed but requires action - skipping delinquency" ,
117+ extra = dict (
118+ stripe_customer_id = invoice .customer ,
119+ stripe_subscription_id = invoice .subscription ,
120+ ),
121+ )
122+ return
123+
86124 log .info (
87125 "Invoice Payment Failed - Setting Delinquency status True" ,
88126 extra = dict (
@@ -137,6 +175,7 @@ def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
137175 ** template_vars ,
138176 )
139177
178+ # handler for Stripe event customer.subscription.deleted
140179 def customer_subscription_deleted (self , subscription : stripe .Subscription ) -> None :
141180 log .info (
142181 "Customer Subscription Deleted - Setting free plan and deactivating repos for stripe customer" ,
@@ -183,6 +222,7 @@ def subscription_schedule_created(
183222 ),
184223 )
185224
225+ # handler for Stripe event subscription_schedule.updated
186226 def subscription_schedule_updated (
187227 self , schedule : stripe .SubscriptionSchedule
188228 ) -> None :
@@ -207,6 +247,7 @@ def subscription_schedule_updated(
207247 ),
208248 )
209249
250+ # handler for Stripe event subscription_schedule.released
210251 def subscription_schedule_released (
211252 self , schedule : stripe .SubscriptionSchedule
212253 ) -> None :
@@ -245,13 +286,15 @@ def subscription_schedule_released(
245286 ),
246287 )
247288
289+ # handler for Stripe event customer.created
248290 def customer_created (self , customer : stripe .Customer ) -> None :
249291 # Based on what stripe doesn't gives us (an ownerid!)
250292 # in this event we cannot reliably create a customer,
251293 # so we're just logging that we created the event and
252294 # relying on customer.subscription.created to handle sub creation
253295 log .info ("Customer created" , extra = dict (stripe_customer_id = customer .id ))
254296
297+ # handler for Stripe event customer.subscription.created
255298 def customer_subscription_created (self , subscription : stripe .Subscription ) -> None :
256299 sub_item_plan_id = subscription .plan .id
257300
@@ -311,6 +354,7 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No
311354
312355 self ._log_updated ([owner ])
313356
357+ # handler for Stripe event customer.subscription.updated
314358 def customer_subscription_updated (self , subscription : stripe .Subscription ) -> None :
315359 owners : QuerySet [Owner ] = Owner .objects .filter (
316360 stripe_subscription_id = subscription .id ,
@@ -341,7 +385,6 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
341385 ),
342386 )
343387 return
344-
345388 # Properly attach the payment method on the customer
346389 # This hook will be called after a checkout session completes,
347390 # updating the subscription created with it
@@ -407,6 +450,7 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
407450 ),
408451 )
409452
453+ # handler for Stripe event customer.updated
410454 def customer_updated (self , customer : stripe .Customer ) -> None :
411455 new_default_payment_method = customer ["invoice_settings" ][
412456 "default_payment_method"
@@ -428,6 +472,7 @@ def customer_updated(self, customer: stripe.Customer) -> None:
428472 subscription ["id" ], default_payment_method = new_default_payment_method
429473 )
430474
475+ # handler for Stripe event checkout.session.completed
431476 def checkout_session_completed (
432477 self , checkout_session : stripe .checkout .Session
433478 ) -> None :
@@ -445,6 +490,58 @@ def checkout_session_completed(
445490
446491 self ._log_updated ([owner ])
447492
493+ def _check_and_handle_delayed_notification_payment_methods (
494+ self , customer_id : str , payment_method_id : str
495+ ):
496+ owner = Owner .objects .get (stripe_customer_id = customer_id )
497+ payment_method = stripe .PaymentMethod .retrieve (payment_method_id )
498+
499+ if payment_method .type == "us_bank_account" and hasattr (
500+ payment_method , "us_bank_account"
501+ ):
502+ # attach the payment method + set as default on the invoice and subscription
503+ stripe .PaymentMethod .attach (
504+ payment_method , customer = owner .stripe_customer_id
505+ )
506+ stripe .Customer .modify (
507+ owner .stripe_customer_id ,
508+ invoice_settings = {"default_payment_method" : payment_method },
509+ )
510+ stripe .Subscription .modify (
511+ owner .stripe_subscription_id , default_payment_method = payment_method
512+ )
513+
514+ # handler for Stripe event payment_intent.succeeded
515+ def payment_intent_succeeded (self , payment_intent : stripe .PaymentIntent ) -> None :
516+ """
517+ Stripe payment intent is used for the initial checkout session
518+ """
519+ log .info (
520+ "Payment intent succeeded" ,
521+ extra = dict (
522+ payment_method_id = payment_intent .id ,
523+ ),
524+ )
525+
526+ self ._check_and_handle_delayed_notification_payment_methods (
527+ payment_intent .customer , payment_intent .payment_method
528+ )
529+
530+ # handler for Stripe event setup_intent.succeeded
531+ def setup_intent_succeeded (self , setup_intent : stripe .SetupIntent ) -> None :
532+ """
533+ Stripe setup intent is used for subsequent edits to payment methods.
534+ See our createSetupIntent api which is called from the UI Stripe Payment Element
535+ """
536+ log .info (
537+ "Setup intent succeeded" ,
538+ extra = dict (setup_intent_id = setup_intent .id ),
539+ )
540+
541+ self ._check_and_handle_delayed_notification_payment_methods (
542+ setup_intent .customer , setup_intent .payment_method
543+ )
544+
448545 def post (self , request : HttpRequest , * args : Any , ** kwargs : Any ) -> Response :
449546 if settings .STRIPE_ENDPOINT_SECRET is None :
450547 log .critical (
@@ -476,3 +573,41 @@ def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Response:
476573 getattr (self , self .event .type .replace ("." , "_" ))(self .event .data .object )
477574
478575 return Response (status = status .HTTP_204_NO_CONTENT )
576+
577+
578+ # TODO - move this
579+ def get_unverified_payment_methods (self , stripe_customer_id : str ):
580+
581+ unverified_payment_methods = []
582+
583+ # Check payment intents
584+ payment_intents = stripe .PaymentIntent .list (customer = stripe_customer_id , limit = 100 )
585+ for intent in payment_intents .data :
586+ if (
587+ hasattr (intent , "next_action" )
588+ and intent .next_action
589+ and intent .next_action .type == "verify_with_microdeposits"
590+ ):
591+ unverified_payment_methods .append (
592+ {
593+ "payment_method_id" : intent .payment_method ,
594+ "hosted_verification_link" : intent .next_action .verify_with_microdeposits .hosted_verification_url ,
595+ }
596+ )
597+
598+ # Check setup intents
599+ setup_intents = stripe .SetupIntent .list (customer = stripe_customer_id , limit = 100 )
600+ for intent in setup_intents .data :
601+ if (
602+ hasattr (intent , "next_action" )
603+ and intent .next_action
604+ and intent .next_action .type == "verify_with_microdeposits"
605+ ):
606+ unverified_payment_methods .append (
607+ {
608+ "payment_method_id" : intent .payment_method ,
609+ "hosted_verification_link" : intent .next_action .verify_with_microdeposits .hosted_verification_url ,
610+ }
611+ )
612+
613+ return unverified_payment_methods
0 commit comments