@@ -37,11 +37,6 @@ def _log_updated(self, updated: List[Owner]) -> None:
37
37
)
38
38
39
39
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
- """
45
40
log .info (
46
41
"Invoice Payment Succeeded - Setting delinquency status False" ,
47
42
extra = dict (
@@ -90,35 +85,24 @@ def invoice_payment_succeeded(self, invoice: stripe.Invoice) -> None:
90
85
91
86
def invoice_payment_failed (self , invoice : stripe .Invoice ) -> None :
92
87
"""
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 .
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 .
96
91
"""
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 :
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" :
104
96
log .info (
105
- "Invoice payment failed but customer has pending payment methods " ,
97
+ "Invoice payment failed but still awaiting known customer action, skipping Delinquency actions " ,
106
98
extra = dict (
107
99
stripe_customer_id = invoice .customer ,
108
100
stripe_subscription_id = invoice .subscription ,
109
- pending_payment_methods = len (unverified_payment_methods ),
101
+ payment_intent_status = payment_intent .status ,
102
+ next_action = payment_intent .next_action ,
110
103
),
111
104
)
112
105
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
106
123
107
log .info (
124
108
"Invoice Payment Failed - Setting Delinquency status True" ,
@@ -176,9 +160,21 @@ def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
176
160
177
161
def customer_subscription_deleted (self , subscription : stripe .Subscription ) -> None :
178
162
"""
179
- Stripe customer.subscription.deleted is called when a subscription is deleted.
180
- This happens when an org goes from paid to free.
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).
181
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
+
182
178
log .info (
183
179
"Customer Subscription Deleted - Setting free plan and deactivating repos for stripe customer" ,
184
180
extra = dict (
@@ -224,7 +220,6 @@ def subscription_schedule_created(
224
220
),
225
221
)
226
222
227
- # handler for Stripe event subscription_schedule.updated
228
223
def subscription_schedule_updated (
229
224
self , schedule : stripe .SubscriptionSchedule
230
225
) -> None :
@@ -249,7 +244,6 @@ def subscription_schedule_updated(
249
244
),
250
245
)
251
246
252
- # handler for Stripe event subscription_schedule.released
253
247
def subscription_schedule_released (
254
248
self , schedule : stripe .SubscriptionSchedule
255
249
) -> None :
@@ -289,24 +283,17 @@ def subscription_schedule_released(
289
283
)
290
284
291
285
def customer_created (self , customer : stripe .Customer ) -> None :
292
- """
293
- Stripe customer.created is called when a customer is created.
294
- This happens when an owner completes a CheckoutSession for the first time.
295
- """
296
286
# Based on what stripe doesn't gives us (an ownerid!)
297
287
# in this event we cannot reliably create a customer,
298
288
# so we're just logging that we created the event and
299
289
# relying on customer.subscription.created to handle sub creation
300
290
log .info ("Customer created" , extra = dict (stripe_customer_id = customer .id ))
301
291
302
- # handler for Stripe event customer.subscription.created
303
292
def customer_subscription_created (self , subscription : stripe .Subscription ) -> None :
304
- log .info (
305
- "Customer subscription created" ,
306
- extra = dict (
307
- customer_id = subscription ["customer" ], subscription_id = subscription ["id" ]
308
- ),
309
- )
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
+ """
310
297
sub_item_plan_id = subscription .plan .id
311
298
312
299
if not sub_item_plan_id :
@@ -349,24 +336,15 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No
349
336
owner .stripe_customer_id = subscription .customer
350
337
owner .save ()
351
338
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
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
370
348
371
349
plan_service = PlanService (current_org = owner )
372
350
plan_service .expire_trial_when_upgrading ()
@@ -385,15 +363,30 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No
385
363
386
364
self ._log_updated ([owner ])
387
365
388
- # handler for Stripe event customer.subscription.updated
389
- def customer_subscription_updated (self , subscription : stripe .Subscription ) -> None :
390
- log .info (
391
- "Customer subscription updated" ,
392
- extra = dict (
393
- customer_id = subscription ["customer" ], subscription_id = subscription ["id" ]
394
- ),
395
- )
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
396
383
384
+ 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
+ """
397
390
owners : QuerySet [Owner ] = Owner .objects .filter (
398
391
stripe_subscription_id = subscription .id ,
399
392
stripe_customer_id = subscription .customer ,
@@ -409,24 +402,15 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
409
402
)
410
403
return
411
404
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
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
430
414
431
415
indication_of_payment_failure = getattr (subscription , "pending_update" , None )
432
416
if indication_of_payment_failure :
@@ -442,6 +426,7 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
442
426
),
443
427
)
444
428
return
429
+
445
430
# Properly attach the payment method on the customer
446
431
# This hook will be called after a checkout session completes,
447
432
# updating the subscription created with it
@@ -507,7 +492,6 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
507
492
),
508
493
)
509
494
510
- # handler for Stripe event customer.updated
511
495
def customer_updated (self , customer : stripe .Customer ) -> None :
512
496
new_default_payment_method = customer ["invoice_settings" ][
513
497
"default_payment_method"
@@ -529,7 +513,6 @@ def customer_updated(self, customer: stripe.Customer) -> None:
529
513
subscription ["id" ], default_payment_method = new_default_payment_method
530
514
)
531
515
532
- # handler for Stripe event checkout.session.completed
533
516
def checkout_session_completed (
534
517
self , checkout_session : stripe .checkout .Session
535
518
) -> None :
@@ -550,12 +533,21 @@ def checkout_session_completed(
550
533
def _check_and_handle_delayed_notification_payment_methods (
551
534
self , customer_id : str , payment_method_id : str
552
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
+ """
553
541
owner = Owner .objects .get (stripe_customer_id = customer_id )
554
542
payment_method = stripe .PaymentMethod .retrieve (payment_method_id )
555
543
556
- if payment_method .type == "us_bank_account" and hasattr (
544
+ is_us_bank_account = payment_method .type == "us_bank_account" and hasattr (
557
545
payment_method , "us_bank_account"
558
- ):
546
+ )
547
+
548
+ should_set_as_default = is_us_bank_account
549
+
550
+ if should_set_as_default :
559
551
# attach the payment method + set as default on the invoice and subscription
560
552
stripe .PaymentMethod .attach (
561
553
payment_method , customer = owner .stripe_customer_id
@@ -570,13 +562,16 @@ def _check_and_handle_delayed_notification_payment_methods(
570
562
571
563
def payment_intent_succeeded (self , payment_intent : stripe .PaymentIntent ) -> None :
572
564
"""
573
- Stripe payment intent is used for the initial checkout session.
574
- Success is emitted when the payment intent goes to a success state.
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.
575
568
"""
576
569
log .info (
577
570
"Payment intent succeeded" ,
578
571
extra = dict (
579
- payment_method_id = payment_intent .id ,
572
+ stripe_customer_id = payment_intent .customer ,
573
+ payment_intent_id = payment_intent .id ,
574
+ payment_method_type = payment_intent .payment_method ,
580
575
),
581
576
)
582
577
@@ -586,12 +581,17 @@ def payment_intent_succeeded(self, payment_intent: stripe.PaymentIntent) -> None
586
581
587
582
def setup_intent_succeeded (self , setup_intent : stripe .SetupIntent ) -> None :
588
583
"""
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
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.
591
587
"""
592
588
log .info (
593
589
"Setup intent succeeded" ,
594
- extra = dict (setup_intent_id = setup_intent .id ),
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
595
)
596
596
597
597
self ._check_and_handle_delayed_notification_payment_methods (
@@ -629,41 +629,3 @@ def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Response:
629
629
getattr (self , self .event .type .replace ("." , "_" ))(self .event .data .object )
630
630
631
631
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