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
@@ -83,6 +84,26 @@ def invoice_payment_succeeded(self, invoice: stripe.Invoice) -> None:
8384 )
8485
8586 def invoice_payment_failed (self , invoice : stripe .Invoice ) -> None :
87+ """
88+ Stripe invoice.payment_failed webhook event is emitted when an invoice payment fails
89+ (initial or recurring). Note that delayed payment methods (including ACH with
90+ microdeposits) may have a failed initial invoice until the account is verified.
91+ """
92+ if invoice .default_payment_method is None :
93+ if invoice .payment_intent :
94+ payment_intent = stripe .PaymentIntent .retrieve (invoice .payment_intent )
95+ if payment_intent .status == "requires_action" :
96+ log .info (
97+ "Invoice payment failed but still awaiting known customer action, skipping Delinquency actions" ,
98+ extra = dict (
99+ stripe_customer_id = invoice .customer ,
100+ stripe_subscription_id = invoice .subscription ,
101+ payment_intent_status = payment_intent .status ,
102+ next_action = payment_intent .next_action ,
103+ ),
104+ )
105+ return
106+
86107 log .info (
87108 "Invoice Payment Failed - Setting Delinquency status True" ,
88109 extra = dict (
@@ -138,6 +159,22 @@ def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
138159 )
139160
140161 def customer_subscription_deleted (self , subscription : stripe .Subscription ) -> None :
162+ """
163+ Stripe customer.subscription.deleted webhook event is emitted when a subscription is deleted.
164+ This happens when an org goes from paid to free (see payment_service.delete_subscription)
165+ or when cleaning up an incomplete subscription that never activated (e.g., abandoned async
166+ ACH microdeposits verification).
167+ """
168+ if subscription .status == "incomplete" :
169+ log .info (
170+ "Customer Subscription Deleted - Ignoring incomplete subscription" ,
171+ extra = dict (
172+ stripe_subscription_id = subscription .id ,
173+ stripe_customer_id = subscription .customer ,
174+ ),
175+ )
176+ return
177+
141178 log .info (
142179 "Customer Subscription Deleted - Setting free plan and deactivating repos for stripe customer" ,
143180 extra = dict (
@@ -253,6 +290,10 @@ def customer_created(self, customer: stripe.Customer) -> None:
253290 log .info ("Customer created" , extra = dict (stripe_customer_id = customer .id ))
254291
255292 def customer_subscription_created (self , subscription : stripe .Subscription ) -> None :
293+ """
294+ Stripe customer.subscription.created webhook event is emitted when a subscription is created.
295+ This happens when an owner completes a CheckoutSession for a new subscription.
296+ """
256297 sub_item_plan_id = subscription .plan .id
257298
258299 if not sub_item_plan_id :
@@ -289,11 +330,22 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No
289330 quantity = subscription .quantity ,
290331 ),
291332 )
333+ # add the subscription_id and customer_id to the owner
292334 owner = Owner .objects .get (ownerid = subscription .metadata .get ("obo_organization" ))
293335 owner .stripe_subscription_id = subscription .id
294336 owner .stripe_customer_id = subscription .customer
295337 owner .save ()
296338
339+ if self ._has_unverified_initial_payment_method (subscription ):
340+ log .info (
341+ "Subscription has pending initial payment verification - will upgrade plan after initial invoice payment" ,
342+ extra = dict (
343+ subscription_id = subscription .id ,
344+ customer_id = subscription .customer ,
345+ ),
346+ )
347+ return
348+
297349 plan_service = PlanService (current_org = owner )
298350 plan_service .expire_trial_when_upgrading ()
299351
@@ -311,7 +363,30 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No
311363
312364 self ._log_updated ([owner ])
313365
366+ def _has_unverified_initial_payment_method (
367+ self , subscription : stripe .Subscription
368+ ) -> bool :
369+ """
370+ Helper method to check if a subscription's latest invoice has a payment intent
371+ that requires verification (e.g. ACH microdeposits)
372+ """
373+ latest_invoice = stripe .Invoice .retrieve (subscription .latest_invoice )
374+ if latest_invoice and latest_invoice .payment_intent :
375+ payment_intent = stripe .PaymentIntent .retrieve (
376+ latest_invoice .payment_intent
377+ )
378+ return (
379+ payment_intent is not None
380+ and payment_intent .status == "requires_action"
381+ )
382+ return False
383+
314384 def customer_subscription_updated (self , subscription : stripe .Subscription ) -> None :
385+ """
386+ Stripe customer.subscription.updated webhook event is emitted when a subscription is updated.
387+ This can happen when an owner updates the subscription's default payment method using our
388+ update_payment_method api
389+ """
315390 owners : QuerySet [Owner ] = Owner .objects .filter (
316391 stripe_subscription_id = subscription .id ,
317392 stripe_customer_id = subscription .customer ,
@@ -327,6 +402,16 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
327402 )
328403 return
329404
405+ if self ._has_unverified_initial_payment_method (subscription ):
406+ log .info (
407+ "Subscription has pending initial payment verification - will upgrade plan after initial invoice payment" ,
408+ extra = dict (
409+ subscription_id = subscription .id ,
410+ customer_id = subscription .customer ,
411+ ),
412+ )
413+ return
414+
330415 indication_of_payment_failure = getattr (subscription , "pending_update" , None )
331416 if indication_of_payment_failure :
332417 # payment failed, raise this to user by setting as delinquent
@@ -445,6 +530,74 @@ def checkout_session_completed(
445530
446531 self ._log_updated ([owner ])
447532
533+ def _check_and_handle_delayed_notification_payment_methods (
534+ self , customer_id : str , payment_method_id : str
535+ ):
536+ """
537+ Helper method to handle payment methods that require delayed verification (like ACH).
538+ When verification succeeds, this attaches the payment method to the customer and sets
539+ it as the default payment method for both the customer and subscription.
540+ """
541+ owner = Owner .objects .get (stripe_customer_id = customer_id )
542+ payment_method = stripe .PaymentMethod .retrieve (payment_method_id )
543+
544+ is_us_bank_account = payment_method .type == "us_bank_account" and hasattr (
545+ payment_method , "us_bank_account"
546+ )
547+
548+ should_set_as_default = is_us_bank_account
549+
550+ if should_set_as_default :
551+ # attach the payment method + set as default on the invoice and subscription
552+ stripe .PaymentMethod .attach (
553+ payment_method , customer = owner .stripe_customer_id
554+ )
555+ stripe .Customer .modify (
556+ owner .stripe_customer_id ,
557+ invoice_settings = {"default_payment_method" : payment_method },
558+ )
559+ stripe .Subscription .modify (
560+ owner .stripe_subscription_id , default_payment_method = payment_method
561+ )
562+
563+ def payment_intent_succeeded (self , payment_intent : stripe .PaymentIntent ) -> None :
564+ """
565+ Stripe payment_intent.succeeded webhook event is emitted when a
566+ payment intent goes to a success state.
567+ We create a Stripe PaymentIntent for the initial checkout session.
568+ """
569+ log .info (
570+ "Payment intent succeeded" ,
571+ extra = dict (
572+ stripe_customer_id = payment_intent .customer ,
573+ payment_intent_id = payment_intent .id ,
574+ payment_method_type = payment_intent .payment_method ,
575+ ),
576+ )
577+
578+ self ._check_and_handle_delayed_notification_payment_methods (
579+ payment_intent .customer , payment_intent .payment_method
580+ )
581+
582+ def setup_intent_succeeded (self , setup_intent : stripe .SetupIntent ) -> None :
583+ """
584+ Stripe setup_intent.succeeded webhook event is emitted when a setup intent
585+ goes to a success state. We create a Stripe SetupIntent for the gazebo UI
586+ PaymentElement to modify payment methods.
587+ """
588+ log .info (
589+ "Setup intent succeeded" ,
590+ extra = dict (
591+ stripe_customer_id = setup_intent .customer ,
592+ setup_intent_id = setup_intent .id ,
593+ payment_method_type = setup_intent .payment_method ,
594+ ),
595+ )
596+
597+ self ._check_and_handle_delayed_notification_payment_methods (
598+ setup_intent .customer , setup_intent .payment_method
599+ )
600+
448601 def post (self , request : HttpRequest , * args : Any , ** kwargs : Any ) -> Response :
449602 if settings .STRIPE_ENDPOINT_SECRET is None :
450603 log .critical (
0 commit comments