Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Commit

Permalink
ENT-3536 | Added a new task for sending the offer usage emails. (#67)
Browse files Browse the repository at this point in the history
  • Loading branch information
hasnain-naveed authored Jul 15, 2020
1 parent 7c1dded commit 8382e63
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 33 deletions.
1 change: 1 addition & 0 deletions ecommerce_worker/configuration/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,6 @@
'templates': {
'course_refund': 'Course Refund',
'assignment_email': 'Offer Assignment Email',
'usage_email': 'Offer Usage Email',
}
}
131 changes: 131 additions & 0 deletions ecommerce_worker/sailthru/v1/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
Notification class for sending the notification emails.
"""
from celery.utils.log import get_task_logger

from sailthru.sailthru_error import SailthruClientError
from ecommerce_worker.sailthru.v1.exceptions import SailthruError
from ecommerce_worker.sailthru.v1.utils import can_retry_sailthru_request, get_sailthru_client

log = get_task_logger(__name__)


class Notification(object):
"""
This class exports the 'send' function for sending the emails.
"""

def __init__(self, config, emails, email_vars, logger_prefix, site_code, template):
self.config = config
self.emails = emails
self.email_vars = email_vars
self.logger_prefix = logger_prefix
self.site_code = site_code
self.template = template

def _is_eligible_for_retry(self, response):
"""
Return a bool whether this task is eligible for retry or not.
Also log the appropriate message according to occurred error.
"""
is_eligible_for_retry = False
error = response.get_error()
log.error(
'[{logger_prefix}] A {token_error_code} - {token_error_message} error occurred while attempting to send a '
'notification. Message: {message}'.format(
logger_prefix=self.logger_prefix,
message=self.email_vars.get('email_body'),
token_error_code=error.get_error_code(),
token_error_message=error.get_message()
)
)
if can_retry_sailthru_request(error):
log.info(
'[{logger_prefix}] An attempt will be made to resend the notification.'
' Message: {message}'.format(
logger_prefix=self.logger_prefix,
message=self.email_vars.get('email_body')
)
)
is_eligible_for_retry = True
else:
log.warning(
'[{logger_prefix}] No further attempts will be made to send the notification.'
' Failed Message: {message}'.format(
logger_prefix=self.logger_prefix,
message=self.email_vars.get('email_body')
)
)
return is_eligible_for_retry

@staticmethod
def _get_send_callback(is_multi_send):
"""
Return the associated function 'send' in case of single send and 'multi_send' in case of multi_send
"""
return 'multi_send' if is_multi_send else 'send'

def _get_client(self):
"""
Return the sailthru client or None if exception raised.
"""
sailthru_client = None
try:
sailthru_client = get_sailthru_client(self.site_code)
except SailthruError:
log.exception(
'[{logger_prefix}] A client error occurred while attempting to send a notification.'
' Message: {message}'.format(
logger_prefix=self.logger_prefix,
message=self.email_vars.get('email_body')
)
)
return sailthru_client

def _get_params(self, is_multi_send):
"""
Return the dict of parameters according to the given 'is_multi_send' parameter.
It can be a simple 'send' function in which we will send an email
to a single email address or it can be a 'multi_send' function.
"""
params = {
'template': self.config['templates'][self.template],
'_vars': self.email_vars
}
email_param = 'emails' if is_multi_send else 'email'
params[email_param] = self.emails
return params

def send(self, is_multi_send=False):
"""
Send the notification email to single email address or comma
separated emails on bases of is_multi_send parameter
Returns:
response: Sailthru endpoint response.
is_eligible_for_retry(Bool): whether this response is eligible for retry or not.
"""
is_eligible_for_retry = False
sailthru_client = self._get_client()
if sailthru_client is None:
return None, is_eligible_for_retry

try:
response = getattr(
sailthru_client,
self._get_send_callback(is_multi_send)
)(**self._get_params(is_multi_send))
except SailthruClientError:
log.exception(
'[{logger_prefix}] A client error occurred while attempting to send a notification.'
' Message: {message}'.format(
logger_prefix=self.logger_prefix,
message=self.email_vars.get('email_body')
)
)
return None, is_eligible_for_retry

if not response.is_ok():
is_eligible_for_retry = self._is_eligible_for_retry(response)
return response, is_eligible_for_retry
57 changes: 36 additions & 21 deletions ecommerce_worker/sailthru/v1/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@

from ecommerce_worker.cache import Cache
from ecommerce_worker.sailthru.v1.exceptions import SailthruError
from ecommerce_worker.sailthru.v1.utils import get_sailthru_client, get_sailthru_configuration
from ecommerce_worker.sailthru.v1.notification import Notification
from ecommerce_worker.sailthru.v1.utils import (
can_retry_sailthru_request,
get_sailthru_client,
get_sailthru_configuration
)
from ecommerce_worker.utils import get_ecommerce_client
from requests.exceptions import RequestException
from six import text_type
Expand Down Expand Up @@ -210,26 +215,6 @@ def _update_unenrolled_list(sailthru_client, email, course_url, unenroll):
return False


def can_retry_sailthru_request(error):
""" Returns True if a Sailthru request and be re-submitted after an error has occurred.
Responses with the following codes can be retried:
9: Internal Error
43: Too many [type] requests this minute to /[endpoint] API
All other errors are considered failures, that should not be retried. A complete list of error codes is available at
https://getstarted.sailthru.com/new-for-developers-overview/api/api-response-errors/.
Args:
error (SailthruResponseError)
Returns:
bool: Indicates if the original request can be retried.
"""
code = error.get_error_code()
return code in (9, 43)


@shared_task(bind=True, ignore_result=True)
def update_course_enrollment(self, email, course_url, purchase_incomplete, mode, unit_cost=None, course_id=None,
currency=None, message_id=None, site_code=None, sku=None):
Expand Down Expand Up @@ -380,6 +365,7 @@ def send_offer_assignment_email(self, user_email, offer_assignment_id, subject,
email_body (str): The body of the email.
site_code (str): Identifier of the site sending the email.
"""
# TODO: Notification class should be used for sending the notification.
config = get_sailthru_configuration(site_code)
response = _send_offer_assignment_notification_email(config, user_email, subject, email_body, site_code, self)
if response and response.is_ok():
Expand Down Expand Up @@ -491,5 +477,34 @@ def send_offer_update_email(self, user_email, subject, email_body, site_code=Non
email_body (str): The body of the email.
site_code (str): Identifier of the site sending the email.
"""
# TODO: Notification class should be used for sending the notification.
config = get_sailthru_configuration(site_code)
_send_offer_assignment_notification_email(config, user_email, subject, email_body, site_code, self)


@shared_task(bind=True, ignore_result=True)
def send_offer_usage_email(self, emails, subject, email_body, site_code=None):
""" Sends the offer usage email.
Args:
self: Ignore.
emails (str): comma separated emails.
subject (str): Email subject.
email_body (str): The body of the email.
site_code (str): Identifier of the site sending the email.
"""
config = get_sailthru_configuration(site_code)

_, is_eligible_for_retry = Notification(
config=config,
emails=emails,
email_vars={
'subject': subject,
'email_body': email_body,
},
logger_prefix="Offer Usage",
site_code=site_code,
template='usage_email'
).send(is_multi_send=True)

if is_eligible_for_retry:
schedule_retry(self, config)
119 changes: 107 additions & 12 deletions ecommerce_worker/sailthru/v1/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
from ecommerce_worker.sailthru.v1.exceptions import SailthruError
from ecommerce_worker.sailthru.v1.tasks import (
update_course_enrollment, _update_unenrolled_list, _get_course_content, _get_course_content_from_ecommerce,
send_course_refund_email, send_offer_assignment_email, send_offer_update_email, _update_assignment_email_status
send_course_refund_email, send_offer_assignment_email, send_offer_update_email, send_offer_usage_email,
_update_assignment_email_status,

)
from ecommerce_worker.utils import get_configuration

Expand Down Expand Up @@ -653,8 +655,24 @@ def test_message_sent(self):
)


class BaseSendEmailTests(TestCase):
"""
Base class for testing the sending notification through sailthru client.
"""

def mock_api_response(self, status, body):
""" Mock the Sailthru send API. """
httpretty.register_uri(
httpretty.POST,
'https://api.sailthru.com/send',
status=status,
body=json.dumps(body),
content_type='application/json'
)


@ddt.ddt
class SendOfferAssignmentEmailTests(TestCase):
class SendOfferAssignmentEmailTests(BaseSendEmailTests):
""" Validates the send_offer_assignment_email task. """
LOG_NAME = 'ecommerce_worker.sailthru.v1.tasks'
USER_EMAIL = '[email protected]'
Expand All @@ -681,16 +699,6 @@ def execute_task(self):
""" Execute the send_offer_assignment_email task. """
send_offer_assignment_email(**self.ASSIGNMENT_TASK_KWARGS)

def mock_api_response(self, status, body):
""" Mock the Sailthru send API. """
httpretty.register_uri(
httpretty.POST,
'https://api.sailthru.com/send',
status=status,
body=json.dumps(body),
content_type='application/json'
)

def mock_ecommerce_assignmentemail_api(self, body, status=200):
""" Mock POST requests to the ecommerce assignmentemail API endpoint. """
httpretty.reset()
Expand Down Expand Up @@ -861,3 +869,90 @@ def test_update_assignment_email_status(self, data, return_value):
'555',
'1234ABC',
'success'), return_value)


class SendOfferUsageEmailTests(BaseSendEmailTests):
""" Validates the send_offer_assignment_email task. """
LOG_NAME = 'ecommerce_worker.sailthru.v1.notification'
EMAILS = '[email protected], [email protected]'
SUBJECT = 'New edX course assignment'
EMAIL_BODY = 'Template message with [email protected] GIL7RUEOU7VHBH7Q ' \
'http://tempurl.url/enroll 3 2012-04-23'
USAGE_TASK_KWARGS = {
'emails': EMAILS,
'subject': SUBJECT,
'email_body': EMAIL_BODY,
}

@patch('ecommerce_worker.sailthru.v1.notification.get_sailthru_client', Mock(side_effect=SailthruError))
def test_client_instantiation_error(self):
""" Verify no message is sent if an error occurs while instantiating the Sailthru API client. """
with LogCapture(level=logging.INFO) as log:
send_offer_usage_email(**self.USAGE_TASK_KWARGS)
log.check(
(self.LOG_NAME, 'ERROR', '[Offer Usage] A client error occurred while attempting to send'
' a notification. Message: {message}'.format(message=self.EMAIL_BODY)),
)

@patch('ecommerce_worker.sailthru.v1.notification.log.exception')
def test_api_client_error(self, mock_log):
""" Verify API client errors are logged. """
with patch.object(SailthruClient, 'multi_send', side_effect=SailthruClientError):
send_offer_usage_email(**self.USAGE_TASK_KWARGS)
mock_log.assert_called_once_with(
'[Offer Usage] A client error occurred while attempting to send a notification.'
' Message: {message}'.format(message=self.EMAIL_BODY)
)

@httpretty.activate
def test_api_error_with_retry(self):
""" Verify the task is rescheduled if an API error occurs, and the request can be retried. """
error_code = 43
error_msg = 'This is a fake error.'
body = {
'error': error_code,
'errormsg': error_msg
}
self.mock_api_response(429, body)
with LogCapture(level=logging.INFO) as log:
with self.assertRaises(Retry):
send_offer_usage_email(**self.USAGE_TASK_KWARGS)
log.check(
(self.LOG_NAME, 'ERROR',
'[Offer Usage] A {token_error_code} - {token_error_message} error occurred'
' while attempting to send a notification.'
' Message: {message}'.format(
message=self.EMAIL_BODY,
token_error_code=error_code,
token_error_message=error_msg
)),
(self.LOG_NAME, 'INFO',
'[Offer Usage] An attempt will be made to resend the notification.'
' Message: {message}'.format(message=self.EMAIL_BODY)),
)

@httpretty.activate
def test_api_error_without_retry(self):
""" Verify error details are logged if an API error occurs, and the request can NOT be retried. """
error_code = 1
error_msg = 'This is a fake error.'
body = {
'error': error_code,
'errormsg': error_msg
}
self.mock_api_response(429, body)
with LogCapture(level=logging.INFO) as log:
send_offer_usage_email(**self.USAGE_TASK_KWARGS)
log.check(
(self.LOG_NAME, 'ERROR',
'[Offer Usage] A {token_error_code} - {token_error_message} error occurred'
' while attempting to send a notification.'
' Message: {message}'.format(
message=self.EMAIL_BODY,
token_error_code=error_code,
token_error_message=error_msg
)),
(self.LOG_NAME, 'WARNING',
'[Offer Usage] No further attempts will be made to send the notification.'
' Failed Message: {message}'.format(message=self.EMAIL_BODY)),
)
20 changes: 20 additions & 0 deletions ecommerce_worker/sailthru/v1/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,23 @@ def get_sailthru_client(site_code):
raise ConfigurationError(msg)

return SailthruClient(key, secret)


def can_retry_sailthru_request(error):
""" Returns True if a Sailthru request and be re-submitted after an error has occurred.
Responses with the following codes can be retried:
9: Internal Error
43: Too many [type] requests this minute to /[endpoint] API
All other errors are considered failures, that should not be retried. A complete list of error codes is available at
https://getstarted.sailthru.com/new-for-developers-overview/api/api-response-errors/.
Args:
error (SailthruResponseError)
Returns:
bool: Indicates if the original request can be retried.
"""
code = error.get_error_code()
return code in (9, 43)

0 comments on commit 8382e63

Please sign in to comment.