Skip to content

Commit d163c61

Browse files
cleanup
1 parent 48b2e89 commit d163c61

File tree

2 files changed

+169
-206
lines changed

2 files changed

+169
-206
lines changed

Diff for: billing/views.py

+95-133
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@ def _log_updated(self, updated: List[Owner]) -> None:
3737
)
3838

3939
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-
"""
4540
log.info(
4641
"Invoice Payment Succeeded - Setting delinquency status False",
4742
extra=dict(
@@ -90,35 +85,24 @@ def invoice_payment_succeeded(self, invoice: stripe.Invoice) -> None:
9085

9186
def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
9287
"""
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.
9691
"""
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":
10496
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",
10698
extra=dict(
10799
stripe_customer_id=invoice.customer,
108100
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,
110103
),
111104
)
112105
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
122106

123107
log.info(
124108
"Invoice Payment Failed - Setting Delinquency status True",
@@ -176,9 +160,21 @@ def invoice_payment_failed(self, invoice: stripe.Invoice) -> None:
176160

177161
def customer_subscription_deleted(self, subscription: stripe.Subscription) -> None:
178162
"""
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).
181167
"""
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+
182178
log.info(
183179
"Customer Subscription Deleted - Setting free plan and deactivating repos for stripe customer",
184180
extra=dict(
@@ -224,7 +220,6 @@ def subscription_schedule_created(
224220
),
225221
)
226222

227-
# handler for Stripe event subscription_schedule.updated
228223
def subscription_schedule_updated(
229224
self, schedule: stripe.SubscriptionSchedule
230225
) -> None:
@@ -249,7 +244,6 @@ def subscription_schedule_updated(
249244
),
250245
)
251246

252-
# handler for Stripe event subscription_schedule.released
253247
def subscription_schedule_released(
254248
self, schedule: stripe.SubscriptionSchedule
255249
) -> None:
@@ -289,24 +283,17 @@ def subscription_schedule_released(
289283
)
290284

291285
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-
"""
296286
# Based on what stripe doesn't gives us (an ownerid!)
297287
# in this event we cannot reliably create a customer,
298288
# so we're just logging that we created the event and
299289
# relying on customer.subscription.created to handle sub creation
300290
log.info("Customer created", extra=dict(stripe_customer_id=customer.id))
301291

302-
# handler for Stripe event customer.subscription.created
303292
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+
"""
310297
sub_item_plan_id = subscription.plan.id
311298

312299
if not sub_item_plan_id:
@@ -349,24 +336,15 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No
349336
owner.stripe_customer_id = subscription.customer
350337
owner.save()
351338

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
370348

371349
plan_service = PlanService(current_org=owner)
372350
plan_service.expire_trial_when_upgrading()
@@ -385,15 +363,30 @@ def customer_subscription_created(self, subscription: stripe.Subscription) -> No
385363

386364
self._log_updated([owner])
387365

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
396383

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+
"""
397390
owners: QuerySet[Owner] = Owner.objects.filter(
398391
stripe_subscription_id=subscription.id,
399392
stripe_customer_id=subscription.customer,
@@ -409,24 +402,15 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
409402
)
410403
return
411404

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
430414

431415
indication_of_payment_failure = getattr(subscription, "pending_update", None)
432416
if indication_of_payment_failure:
@@ -442,6 +426,7 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
442426
),
443427
)
444428
return
429+
445430
# Properly attach the payment method on the customer
446431
# This hook will be called after a checkout session completes,
447432
# updating the subscription created with it
@@ -507,7 +492,6 @@ def customer_subscription_updated(self, subscription: stripe.Subscription) -> No
507492
),
508493
)
509494

510-
# handler for Stripe event customer.updated
511495
def customer_updated(self, customer: stripe.Customer) -> None:
512496
new_default_payment_method = customer["invoice_settings"][
513497
"default_payment_method"
@@ -529,7 +513,6 @@ def customer_updated(self, customer: stripe.Customer) -> None:
529513
subscription["id"], default_payment_method=new_default_payment_method
530514
)
531515

532-
# handler for Stripe event checkout.session.completed
533516
def checkout_session_completed(
534517
self, checkout_session: stripe.checkout.Session
535518
) -> None:
@@ -550,12 +533,21 @@ def checkout_session_completed(
550533
def _check_and_handle_delayed_notification_payment_methods(
551534
self, customer_id: str, payment_method_id: str
552535
):
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+
"""
553541
owner = Owner.objects.get(stripe_customer_id=customer_id)
554542
payment_method = stripe.PaymentMethod.retrieve(payment_method_id)
555543

556-
if payment_method.type == "us_bank_account" and hasattr(
544+
is_us_bank_account = payment_method.type == "us_bank_account" and hasattr(
557545
payment_method, "us_bank_account"
558-
):
546+
)
547+
548+
should_set_as_default = is_us_bank_account
549+
550+
if should_set_as_default:
559551
# attach the payment method + set as default on the invoice and subscription
560552
stripe.PaymentMethod.attach(
561553
payment_method, customer=owner.stripe_customer_id
@@ -570,13 +562,16 @@ def _check_and_handle_delayed_notification_payment_methods(
570562

571563
def payment_intent_succeeded(self, payment_intent: stripe.PaymentIntent) -> None:
572564
"""
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.
575568
"""
576569
log.info(
577570
"Payment intent succeeded",
578571
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,
580575
),
581576
)
582577

@@ -586,12 +581,17 @@ def payment_intent_succeeded(self, payment_intent: stripe.PaymentIntent) -> None
586581

587582
def setup_intent_succeeded(self, setup_intent: stripe.SetupIntent) -> None:
588583
"""
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.
591587
"""
592588
log.info(
593589
"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+
),
595595
)
596596

597597
self._check_and_handle_delayed_notification_payment_methods(
@@ -629,41 +629,3 @@ def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Response:
629629
getattr(self, self.event.type.replace(".", "_"))(self.event.data.object)
630630

631631
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

Comments
 (0)