Skip to content

Commit cde61fa

Browse files
zamanafzaliloveagent57
authored andcommitted
feat: SSP payment receipt email
1 parent 387a21b commit cde61fa

File tree

4 files changed

+298
-1
lines changed

4 files changed

+298
-1
lines changed

enterprise_access/apps/customer_billing/stripe_event_handlers.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
from enterprise_access.apps.customer_billing.models import CheckoutIntent, StripeEventData
1111
from enterprise_access.apps.customer_billing.stripe_event_types import StripeEventType
12-
from enterprise_access.apps.customer_billing.tasks import send_trial_cancellation_email_task
12+
from enterprise_access.apps.customer_billing.tasks import send_payment_receipt_email, send_trial_cancellation_email_task
1313

1414
logger = logging.getLogger(__name__)
1515

@@ -176,6 +176,13 @@ def invoice_paid(event: stripe.Event) -> None:
176176
checkout_intent.mark_as_paid(stripe_customer_id=stripe_customer_id)
177177
link_event_data_to_checkout_intent(event, checkout_intent)
178178

179+
send_payment_receipt_email.delay(
180+
invoice_data=invoice,
181+
subscription_data=subscription_details,
182+
enterprise_customer_name=checkout_intent.enterprise_name,
183+
enterprise_slug=checkout_intent.enterprise_slug,
184+
)
185+
179186
@on_stripe_event('customer.subscription.trial_will_end')
180187
@staticmethod
181188
def trial_will_end(event: stripe.Event) -> None:

enterprise_access/apps/customer_billing/tasks.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from enterprise_access.apps.api_client.braze_client import BrazeApiClient
1313
from enterprise_access.apps.api_client.lms_client import LmsApiClient
14+
from enterprise_access.apps.content_assignments.content_metadata_api import format_datetime_obj
1415
from enterprise_access.apps.customer_billing.api import create_stripe_billing_portal_session
1516
from enterprise_access.apps.customer_billing.models import CheckoutIntent
1617
from enterprise_access.apps.provisioning.utils import validate_trial_subscription
@@ -20,6 +21,128 @@
2021
logger = logging.getLogger(__name__)
2122

2223

24+
@shared_task(base=LoggedTaskWithRetry)
25+
def send_payment_receipt_email(
26+
invoice_data,
27+
subscription_data,
28+
enterprise_customer_name,
29+
enterprise_slug,
30+
):
31+
"""
32+
Send payment receipt emails to enterprise admins after successful payment.
33+
34+
Args:
35+
invoice_data (dict): The Stripe invoice data containing payment details
36+
subscription_data (dict): The Stripe subscription data
37+
enterprise_customer_name (str): Name of the enterprise organization
38+
enterprise_slug (str): URL-friendly slug for the enterprise
39+
40+
Raises:
41+
BrazeClientError: If there's an error communicating with Braze
42+
Exception: For any other unexpected errors during email sending
43+
"""
44+
logger.info(
45+
'Sending payment receipt confirmation email for enterprise %s (slug: %s)',
46+
enterprise_customer_name,
47+
enterprise_slug,
48+
)
49+
50+
braze_client = BrazeApiClient()
51+
lms_client = LmsApiClient()
52+
53+
enterprise_data = lms_client.get_enterprise_customer_data(enterprise_customer_slug=enterprise_slug)
54+
admin_users = enterprise_data.get('admin_users', [])
55+
56+
if not admin_users:
57+
logger.error(
58+
'Payment receipt confirmation email not sent: No admin users found for enterprise %s (slug: %s)',
59+
enterprise_customer_name,
60+
enterprise_slug,
61+
)
62+
return
63+
64+
# Format the payment date
65+
payment_date = datetime.fromtimestamp(invoice_data.get('created', 0))
66+
formatted_date = format_datetime_obj(payment_date, '%d %B %Y')
67+
68+
# Get payment method details
69+
payment_method = invoice_data.get('payment_intent', {}).get('payment_method', {})
70+
card_details = payment_method.get('card', {})
71+
payment_method_display = f"{card_details.get('brand', 'Card')} - {card_details.get('last4', '****')}"
72+
73+
# Get subscription details
74+
quantity = subscription_data.get('quantity', 0)
75+
price_per_license = subscription_data.get('plan', {}).get('amount', 0)
76+
total_amount = quantity * price_per_license
77+
78+
# Get billing address
79+
billing_details = payment_method.get('billing_details', {})
80+
address = billing_details.get('address', {})
81+
billing_address = '\n'.join(filter(None, [
82+
address.get('line1', ''),
83+
address.get('line2', ''),
84+
f"{address.get('city', '')}, {address.get('state', '')} {address.get('postal_code', '')}",
85+
address.get('country', '')
86+
]))
87+
88+
braze_trigger_properties = {
89+
'total_paid_amount': cents_to_dollars(total_amount),
90+
'date_paid': formatted_date,
91+
'payment_method': payment_method_display,
92+
'license_count': quantity,
93+
'price_per_license': cents_to_dollars(price_per_license),
94+
'customer_name': billing_details.get('name', ''),
95+
'organization': enterprise_customer_name,
96+
'billing_address': billing_address,
97+
'enterprise_admin_portal_url': f'{settings.ENTERPRISE_ADMIN_PORTAL_URL}/{enterprise_slug}',
98+
'receipt_number': invoice_data.get('id', ''),
99+
}
100+
101+
recipients = []
102+
for admin in admin_users:
103+
try:
104+
admin_email = admin['email']
105+
recipient = braze_client.create_braze_recipient(
106+
user_email=admin_email,
107+
lms_user_id=admin.get('lms_user_id'),
108+
)
109+
recipients.append(recipient)
110+
except Exception as exc:
111+
logger.warning(
112+
'Failed to create Braze recipient for admin email %s: %s',
113+
admin_email,
114+
str(exc)
115+
)
116+
117+
if not recipients:
118+
logger.error(
119+
'Payment receipt confirmation email not sent: No valid Braze recipients created for enterprise %s.'
120+
' Check admin email errors above.',
121+
enterprise_customer_name
122+
)
123+
return
124+
125+
try:
126+
braze_client.send_campaign_message(
127+
settings.BRAZE_ENTERPRISE_PROVISION_PAYMENT_RECEIPT_CAMPAIGN,
128+
recipients=recipients,
129+
trigger_properties=braze_trigger_properties,
130+
)
131+
logger.info(
132+
'Successfully sent payment receipt confirmation emails for enterprise %s to %d recipients',
133+
enterprise_customer_name,
134+
len(recipients)
135+
)
136+
137+
except Exception as exc:
138+
logger.exception(
139+
'Braze API error: Failed to send payment receipt confirmation email for enterprise %s. Error: %s',
140+
enterprise_customer_name,
141+
str(exc)
142+
)
143+
raise
144+
145+
23146
@shared_task(base=LoggedTaskWithRetry)
24147
def send_enterprise_provision_signup_confirmation_email(
25148
subscription_start_date: str,

enterprise_access/apps/customer_billing/tests/test_tasks.py

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from enterprise_access.apps.customer_billing.models import CheckoutIntent
1313
from enterprise_access.apps.customer_billing.tasks import (
1414
send_enterprise_provision_signup_confirmation_email,
15+
send_payment_receipt_email,
1516
send_trial_cancellation_email_task
1617
)
1718

@@ -245,3 +246,168 @@ def test_braze_campaign_send_failure(self, mock_validate_trial, mock_lms_client,
245246
send_enterprise_provision_signup_confirmation_email(**self.test_data)
246247

247248
self.assertEqual(str(context.exception), "Braze Campaign Error")
249+
250+
251+
class TestSendPaymentReceiptEmail(TestCase):
252+
"""
253+
Tests for send_payment_receipt_email task.
254+
"""
255+
def setUp(self):
256+
super().setUp()
257+
self.mock_invoice_data = {
258+
'id': 'in_1SNvVOQ60jNALKNUMk8TZucs',
259+
'created': 1761829387,
260+
'payment_intent': {
261+
'payment_method': {
262+
'card': {
263+
'brand': 'visa',
264+
'last4': '4242'
265+
},
266+
'billing_details': {
267+
'name': 'Test User',
268+
'address': {
269+
'line1': '123 Test St',
270+
'line2': 'Suite 100',
271+
'city': 'Test City',
272+
'state': 'TS',
273+
'postal_code': '12345',
274+
'country': 'US'
275+
}
276+
}
277+
}
278+
}
279+
}
280+
self.mock_subscription_data = {
281+
'quantity': 5,
282+
'plan': {
283+
'amount': 39600 # $396.00 in cents
284+
}
285+
}
286+
self.mock_admin_users = [
287+
{
288+
'email': '[email protected]',
289+
'lms_user_id': 1,
290+
},
291+
{
292+
'email': '[email protected]',
293+
'lms_user_id': 2,
294+
}
295+
]
296+
self.enterprise_customer_name = 'Test Enterprise'
297+
self.enterprise_slug = 'test-enterprise'
298+
299+
@mock.patch('enterprise_access.apps.customer_billing.tasks.format_datetime_obj')
300+
@mock.patch('enterprise_access.apps.customer_billing.tasks.BrazeApiClient')
301+
@mock.patch('enterprise_access.apps.customer_billing.tasks.LmsApiClient')
302+
def test_successful_payment_receipt_email(self, mock_lms_client, mock_braze_client, mock_format_datetime):
303+
"""
304+
Test successful payment receipt email sending.
305+
"""
306+
# Mock the date formatting function
307+
mock_format_datetime.return_value = '03 November 2025'
308+
309+
mock_lms_client.return_value.get_enterprise_customer_data.return_value = {
310+
'admin_users': self.mock_admin_users
311+
}
312+
313+
mock_braze = mock_braze_client.return_value
314+
braze_recipients = []
315+
actual_calls = []
316+
317+
def create_recipient_side_effect(user_email, lms_user_id):
318+
actual_calls.append(mock.call(user_email=user_email, lms_user_id=lms_user_id))
319+
recipient = {'external_id': f'braze_{lms_user_id}'}
320+
braze_recipients.append(recipient)
321+
return recipient
322+
mock_braze.create_braze_recipient.side_effect = create_recipient_side_effect
323+
324+
# Call the task
325+
send_payment_receipt_email(
326+
invoice_data=self.mock_invoice_data,
327+
subscription_data=self.mock_subscription_data,
328+
enterprise_customer_name=self.enterprise_customer_name,
329+
enterprise_slug=self.enterprise_slug,
330+
)
331+
332+
# Verify LMS API was called to get admin users
333+
mock_lms_client.return_value.get_enterprise_customer_data.assert_called_once_with(
334+
enterprise_customer_slug=self.enterprise_slug
335+
)
336+
337+
# Verify Braze recipients were created for each admin
338+
expected_recipient_calls = [
339+
mock.call(user_email=admin['email'], lms_user_id=admin.get('lms_user_id'))
340+
for admin in self.mock_admin_users
341+
]
342+
mock_braze.create_braze_recipient.assert_has_calls(expected_recipient_calls, any_order=True)
343+
344+
# Verify the campaign was sent with correct properties
345+
expected_properties = {
346+
'total_paid_amount': Decimal('1980.00'), # $396.00 * 5 licenses = $1,980.00
347+
'date_paid': '03 November 2025', # Based on mock timestamp
348+
'payment_method': 'visa - 4242',
349+
'license_count': 5,
350+
'price_per_license': Decimal('396.00'),
351+
'customer_name': 'Test User',
352+
'organization': 'Test Enterprise',
353+
'billing_address': '123 Test St\nSuite 100\nTest City, TS 12345\nUS',
354+
'enterprise_admin_portal_url': f'{settings.ENTERPRISE_ADMIN_PORTAL_URL}/test-enterprise',
355+
'receipt_number': 'in_1SNvVOQ60jNALKNUMk8TZucs',
356+
}
357+
358+
mock_braze.send_campaign_message.assert_called_once_with(
359+
settings.BRAZE_ENTERPRISE_PROVISION_PAYMENT_RECEIPT_CAMPAIGN,
360+
recipients=braze_recipients,
361+
trigger_properties=expected_properties,
362+
)
363+
364+
@mock.patch('enterprise_access.apps.customer_billing.tasks.BrazeApiClient')
365+
@mock.patch('enterprise_access.apps.customer_billing.tasks.LmsApiClient')
366+
def test_payment_receipt_no_admin_users(self, mock_lms_client, mock_braze_client):
367+
"""
368+
Test that email is not sent when no admin users are found.
369+
"""
370+
mock_lms_client.return_value.get_enterprise_customer_data.return_value = {
371+
'admin_users': []
372+
}
373+
374+
send_payment_receipt_email(
375+
invoice_data=self.mock_invoice_data,
376+
subscription_data=self.mock_subscription_data,
377+
enterprise_customer_name=self.enterprise_customer_name,
378+
enterprise_slug=self.enterprise_slug,
379+
)
380+
381+
# Verify LMS API was called but Braze API was not
382+
mock_lms_client.return_value.get_enterprise_customer_data.assert_called_once()
383+
mock_braze_client.return_value.send_campaign_message.assert_not_called()
384+
385+
@mock.patch('enterprise_access.apps.customer_billing.tasks.BrazeApiClient')
386+
@mock.patch('enterprise_access.apps.customer_billing.tasks.LmsApiClient')
387+
def test_payment_receipt_braze_recipient_error(self, mock_lms_client, mock_braze_client):
388+
"""
389+
Test handling of Braze recipient creation errors.
390+
"""
391+
mock_lms_client.return_value.get_enterprise_customer_data.return_value = {
392+
'admin_users': self.mock_admin_users
393+
}
394+
395+
# Make first recipient creation fail, second one succeed
396+
mock_braze = mock_braze_client.return_value
397+
mock_braze.create_braze_recipient.side_effect = [
398+
Exception("Failed to create recipient"),
399+
{'external_id': 'braze_2'}
400+
]
401+
402+
send_payment_receipt_email(
403+
invoice_data=self.mock_invoice_data,
404+
subscription_data=self.mock_subscription_data,
405+
enterprise_customer_name=self.enterprise_customer_name,
406+
enterprise_slug=self.enterprise_slug,
407+
)
408+
409+
# Verify campaign was still sent for the successful recipient
410+
mock_braze.send_campaign_message.assert_called_once()
411+
actual_recipients = mock_braze.send_campaign_message.call_args[1]['recipients']
412+
self.assertEqual(len(actual_recipients), 1)
413+
self.assertEqual(actual_recipients[0]['external_id'], 'braze_2')

enterprise_access/settings/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,7 @@ def root(*path_fragments):
508508

509509
# Braze campaigns for enterprise provisioning (apps.provisioning)
510510
BRAZE_ENTERPRISE_PROVISION_SIGNUP_CONFIRMATION_CAMPAIGN = ''
511+
BRAZE_ENTERPRISE_PROVISION_PAYMENT_RECEIPT_CAMPAIGN = ''
511512

512513
# Braze campaigns for browse and request (apps.subsidy_request)
513514
BRAZE_NEW_REQUESTS_NOTIFICATION_CAMPAIGN = ''

0 commit comments

Comments
 (0)