@@ -38,9 +38,10 @@ def _log_updated(self, updated: List[Owner]) -> None:
38
38
39
39
def invoice_payment_succeeded (self , invoice : stripe .Invoice ) -> None :
40
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)
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)
44
45
"""
45
46
log .info (
46
47
"Invoice Payment Succeeded - Setting delinquency status False" ,
@@ -90,35 +91,27 @@ def invoice_payment_succeeded(self, invoice: stripe.Invoice) -> None:
90
91
91
92
def invoice_payment_failed (self , invoice : stripe .Invoice ) -> None :
92
93
"""
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.
96
98
"""
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" :
104
105
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 " ,
106
107
extra = dict (
107
108
stripe_customer_id = invoice .customer ,
108
109
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 ,
110
112
),
111
113
)
112
114
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
122
115
123
116
log .info (
124
117
"Invoice Payment Failed - Setting Delinquency status True" ,
@@ -176,9 +169,21 @@ def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
176
169
177
170
def customer_subscription_deleted (self , subscription : stripe .Subscription ) -> None :
178
171
"""
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).
181
176
"""
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
+
182
187
log .info (
183
188
"Customer Subscription Deleted - Setting free plan and deactivating repos for stripe customer" ,
184
189
extra = dict (
@@ -224,7 +229,6 @@ def subscription_schedule_created(
224
229
),
225
230
)
226
231
227
- # handler for Stripe event subscription_schedule.updated
228
232
def subscription_schedule_updated (
229
233
self , schedule : stripe .SubscriptionSchedule
230
234
) -> None :
@@ -249,7 +253,6 @@ def subscription_schedule_updated(
249
253
),
250
254
)
251
255
252
- # handler for Stripe event subscription_schedule.released
253
256
def subscription_schedule_released (
254
257
self , schedule : stripe .SubscriptionSchedule
255
258
) -> None :
@@ -290,7 +293,7 @@ def subscription_schedule_released(
290
293
291
294
def customer_created (self , customer : stripe .Customer ) -> None :
292
295
"""
293
- Stripe customer.created is called when a customer is created.
296
+ Stripe customer.created webhook event is emitted when a customer is created.
294
297
This happens when an owner completes a CheckoutSession for the first time.
295
298
"""
296
299
# Based on what stripe doesn't gives us (an ownerid!)
@@ -299,8 +302,11 @@ def customer_created(self, customer: stripe.Customer) -> None:
299
302
# relying on customer.subscription.created to handle sub creation
300
303
log .info ("Customer created" , extra = dict (stripe_customer_id = customer .id ))
301
304
302
- # handler for Stripe event customer.subscription.created
303
305
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
+ """
304
310
log .info (
305
311
"Customer subscription created" ,
306
312
extra = dict (
@@ -349,24 +355,15 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No
349
355
owner .stripe_customer_id = subscription .customer
350
356
owner .save ()
351
357
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
370
367
371
368
plan_service = PlanService (current_org = owner )
372
369
plan_service .expire_trial_when_upgrading ()
@@ -385,8 +382,31 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No
385
382
386
383
self ._log_updated ([owner ])
387
384
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
+
389
403
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
+ """
390
410
log .info (
391
411
"Customer subscription updated" ,
392
412
extra = dict (
@@ -409,24 +429,15 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
409
429
)
410
430
return
411
431
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
430
441
431
442
indication_of_payment_failure = getattr (subscription , "pending_update" , None )
432
443
if indication_of_payment_failure :
@@ -442,6 +453,7 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
442
453
),
443
454
)
444
455
return
456
+
445
457
# Properly attach the payment method on the customer
446
458
# This hook will be called after a checkout session completes,
447
459
# updating the subscription created with it
@@ -507,7 +519,6 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
507
519
),
508
520
)
509
521
510
- # handler for Stripe event customer.updated
511
522
def customer_updated (self , customer : stripe .Customer ) -> None :
512
523
new_default_payment_method = customer ["invoice_settings" ][
513
524
"default_payment_method"
@@ -529,7 +540,6 @@ def customer_updated(self, customer: stripe.Customer) -> None:
529
540
subscription ["id" ], default_payment_method = new_default_payment_method
530
541
)
531
542
532
- # handler for Stripe event checkout.session.completed
533
543
def checkout_session_completed (
534
544
self , checkout_session : stripe .checkout .Session
535
545
) -> None :
@@ -550,12 +560,21 @@ def checkout_session_completed(
550
560
def _check_and_handle_delayed_notification_payment_methods (
551
561
self , customer_id : str , payment_method_id : str
552
562
):
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
+ """
553
568
owner = Owner .objects .get (stripe_customer_id = customer_id )
554
569
payment_method = stripe .PaymentMethod .retrieve (payment_method_id )
555
570
556
- if payment_method .type == "us_bank_account" and hasattr (
571
+ is_us_bank_account = payment_method .type == "us_bank_account" and hasattr (
557
572
payment_method , "us_bank_account"
558
- ):
573
+ )
574
+
575
+ should_set_as_default = is_us_bank_account
576
+
577
+ if should_set_as_default :
559
578
# attach the payment method + set as default on the invoice and subscription
560
579
stripe .PaymentMethod .attach (
561
580
payment_method , customer = owner .stripe_customer_id
@@ -570,13 +589,16 @@ def _check_and_handle_delayed_notification_payment_methods(
570
589
571
590
def payment_intent_succeeded (self , payment_intent : stripe .PaymentIntent ) -> None :
572
591
"""
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.
575
595
"""
576
596
log .info (
577
597
"Payment intent succeeded" ,
578
598
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 ,
580
602
),
581
603
)
582
604
@@ -586,12 +608,17 @@ def payment_intent_succeeded(self, payment_intent: stripe.PaymentIntent) -> None
586
608
587
609
def setup_intent_succeeded (self , setup_intent : stripe .SetupIntent ) -> None :
588
610
"""
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.
591
614
"""
592
615
log .info (
593
616
"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
+ ),
595
622
)
596
623
597
624
self ._check_and_handle_delayed_notification_payment_methods (
@@ -629,41 +656,3 @@ def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Response:
629
656
getattr (self , self .event .type .replace ("." , "_" ))(self .event .data .object )
630
657
631
658
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