@@ -288,3 +288,110 @@ def send_trial_cancellation_email_task(
288288 str (exc ),
289289 )
290290 raise
291+
292+
293+ @shared_task (base = LoggedTaskWithRetry )
294+ def send_billing_error_email_task (checkout_intent_id : int ):
295+ """
296+ Send Braze email notification when a subscription encounters a billing error
297+ (e.g., transitions to past_due).
298+
299+ The email includes a link to the Stripe billing portal so admins can fix their
300+ payment method and restart the subscription.
301+
302+ Args:
303+ checkout_intent_id (int): ID of the CheckoutIntent record
304+ """
305+ try :
306+ checkout_intent = CheckoutIntent .objects .get (id = checkout_intent_id )
307+ except CheckoutIntent .DoesNotExist :
308+ logger .error (
309+ "Billing error email not sent: CheckoutIntent %s not found" ,
310+ checkout_intent_id ,
311+ )
312+ return
313+
314+ enterprise_slug = checkout_intent .enterprise_slug
315+ logger .info (
316+ "Sending billing error email for CheckoutIntent %s (enterprise slug: %s)" ,
317+ checkout_intent_id ,
318+ enterprise_slug ,
319+ )
320+
321+ braze_client = BrazeApiClient ()
322+ lms_client = LmsApiClient ()
323+
324+ # Fetch enterprise customer data to get admin users
325+ try :
326+ enterprise_data = lms_client .get_enterprise_customer_data (
327+ enterprise_customer_slug = enterprise_slug
328+ )
329+ except Exception as exc : # pylint: disable=broad-exception-caught
330+ logger .error (
331+ "Failed to fetch enterprise data for slug %s: %s. Cannot send billing error email." ,
332+ enterprise_slug ,
333+ str (exc ),
334+ )
335+ return
336+
337+ admin_users = enterprise_data .get ("admin_users" , [])
338+
339+ if not admin_users :
340+ logger .error (
341+ "Billing error email not sent: No admin users found for enterprise slug %s. "
342+ "Verify admin setup in LMS." ,
343+ enterprise_slug ,
344+ )
345+ return
346+
347+ # Generate Stripe billing portal URL for fixing payment method
348+ portal_url = _get_billing_portal_url (checkout_intent )
349+
350+ braze_trigger_properties = {
351+ "restart_subscription_url" : portal_url ,
352+ }
353+
354+ # Create Braze recipients for all admin users
355+ recipients = []
356+ for admin in admin_users :
357+ try :
358+ admin_email = admin ["email" ]
359+ recipient = braze_client .create_braze_recipient (
360+ user_email = admin_email ,
361+ lms_user_id = admin .get ("lms_user_id" ),
362+ )
363+ recipients .append (recipient )
364+ except Exception as exc : # pylint: disable=broad-exception-caught
365+ logger .warning (
366+ "Failed to create Braze recipient for admin email %s: %s" ,
367+ admin_email ,
368+ str (exc ),
369+ )
370+
371+ if not recipients :
372+ logger .error (
373+ "Billing error email not sent: No valid Braze recipients created for enterprise slug %s. "
374+ "Check admin email errors above." ,
375+ enterprise_slug ,
376+ )
377+ return
378+
379+ # Send the campaign message to all admin recipients
380+ try :
381+ braze_client .send_campaign_message (
382+ settings .BRAZE_BILLING_ERROR_CAMPAIGN ,
383+ recipients = recipients ,
384+ trigger_properties = braze_trigger_properties ,
385+ )
386+ logger .info (
387+ "Successfully sent billing error emails for CheckoutIntent %s to %d recipients" ,
388+ checkout_intent_id ,
389+ len (recipients ),
390+ )
391+ except Exception as exc : # pylint: disable=broad-exception-caught
392+ logger .exception (
393+ "Braze API error: Failed to send billing error email for CheckoutIntent %s. Error: %s" ,
394+ checkout_intent_id ,
395+ str (exc ),
396+ )
397+ raise
0 commit comments