Commit 2eaef9ac by Clinton Blackburn

Added task to send course refund email via Sailthru

ECOM-6975
parent f45f0989
......@@ -16,13 +16,14 @@ help:
@echo ' '
requirements:
pip install -qr requirements/local.txt --exists-action w
pip install -r requirements/local.txt
worker:
celery -A ecommerce_worker worker --app=$(PACKAGE).celery_app:app --loglevel=info --queue=fulfillment,email_marketing
test:
WORKER_CONFIGURATION_MODULE=ecommerce_worker.configuration.test nosetests \
--with-ignore-docstrings --logging-level=DEBUG --logging-clear-handlers \
--with-coverage --cover-branches --cover-html --cover-package=$(PACKAGE) $(PACKAGE)
html_coverage:
......
......@@ -87,5 +87,9 @@ SAILTHRU = {
# dummy price for audit/honor (i.e., if cost = 0)
# Note: setting this value to 0 skips Sailthru calls for free transactions
'SAILTHRU_MINIMUM_COST': 100,
}
# Transactional email template name map
'templates': {
'course_refund': 'Course Refund',
}
}
......@@ -304,3 +304,67 @@ def update_course_enrollment(self, email, course_url, purchase_incomplete, mode,
if not _record_purchase(sailthru_client, email, item, purchase_incomplete, message_id, options):
schedule_retry(self, config)
@shared_task(bind=True, ignore_result=True)
def send_course_refund_email(self, email, refund_id, amount, course_name, order_number, order_url, site_code=None):
""" Sends the course refund email.
Args:
self: Ignore.
email (str): Recipient's email address.
refund_id (int): ID of the refund that initiated this task.
amount (str): Formatted amount of the refund.
course_name (str): Name of the course for which payment was refunded.
order_number (str): Order number of the order that was refunded.
order_url (str): Receipt URL of the refunded order.
site_code (str): Identifier of the site sending the email.
"""
config = get_sailthru_configuration(site_code)
try:
sailthru_client = get_sailthru_client(site_code)
except SailthruError:
# NOTE: We rely on the function to log the error for us
return
email_vars = {
'amount': amount,
'course_name': course_name,
'order_number': order_number,
'order_url': order_url,
}
try:
response = sailthru_client.send(
template=config['templates']['course_refund'],
email=email,
_vars=email_vars
)
except SailthruClientError:
logger.exception(
'A client error occurred while attempting to send a course refund notification for refund [%d].',
refund_id
)
return
if response.is_ok():
logger.info('Course refund notification sent for refund %d.', refund_id)
else:
error = response.get_error()
logger.error(
'An error occurred while attempting to send a course refund notification for refund [%d]: %d - %s',
refund_id, error.get_error_code(), error.get_message()
)
if can_retry_sailthru_request(error):
logger.info(
'An attempt will be made again to send a course refund notification for refund [%d].',
refund_id
)
schedule_retry(self, config)
else:
logger.warning(
'No further attempts will be made to send a course refund notification for refund [%d].',
refund_id
)
"""Tests of Sailthru worker code."""
import json
import logging
from decimal import Decimal
from unittest import TestCase
import httpretty
from celery.exceptions import Retry
from mock import patch
from sailthru import SailthruClient
from sailthru.sailthru_error import SailthruClientError
from testfixtures import LogCapture
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
update_course_enrollment, _update_unenrolled_list, _get_course_content, _get_course_content_from_ecommerce,
send_course_refund_email
)
from ecommerce_worker.sailthru.v1.utils import get_sailthru_configuration
from ecommerce_worker.utils import get_configuration
......@@ -573,3 +577,98 @@ class SailthruTests(TestCase):
mock_sailthru_client.api_get.side_effect = SailthruClientError
self.assertFalse(_update_unenrolled_list(mock_sailthru_client, TEST_EMAIL,
self.course_url, False))
class SendCourseRefundEmailTests(TestCase):
""" Validates the send_course_refund_email task. """
LOG_NAME = 'ecommerce_worker.sailthru.v1.tasks'
REFUND_ID = 1
SITE_CODE = 'test'
def execute_task(self):
""" Execute the send_course_refund_email task. """
send_course_refund_email(
'test@example.com', self.REFUND_ID, '$150.00', 'Test Course', 'EDX-123', 'http://example.com', None
)
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 test_client_instantiation_error(self):
""" Verify no message is sent if an error occurs while instantiating the Sailthru API client. """
with patch('ecommerce_worker.sailthru.v1.utils.get_sailthru_client', raises=SailthruError):
self.execute_task()
@patch('ecommerce_worker.sailthru.v1.tasks.logger.exception')
def test_api_client_error(self, mock_log):
""" Verify API client errors are logged. """
with patch.object(SailthruClient, 'send', side_effect=SailthruClientError):
self.execute_task()
mock_log.assert_called_once_with(
'A client error occurred while attempting to send a course refund notification for refund [%d].',
self.REFUND_ID
)
@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() as log:
with self.assertRaises(Retry):
self.execute_task()
log.check(
(self.LOG_NAME, 'ERROR',
'An error occurred while attempting to send a course refund notification for refund [%d]: %d - %s' % (
self.REFUND_ID, error_code, error_msg)),
(self.LOG_NAME, 'INFO',
'An attempt will be made again to send a course refund notification for refund [%d].' % self.REFUND_ID),
)
@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(500, body)
with LogCapture() as log:
self.execute_task()
log.check(
(self.LOG_NAME, 'ERROR',
'An error occurred while attempting to send a course refund notification for refund [%d]: %d - %s' % (
self.REFUND_ID, error_code, error_msg)),
(self.LOG_NAME, 'WARNING',
'No further attempts will be made to send a course refund notification for refund [%d].' % self.REFUND_ID),
)
@httpretty.activate
def test_message_sent(self):
""" Verify a message is logged after a successful API call to send the message. """
self.mock_api_response(200, {'send_id': '1234ABC'})
with LogCapture() as log:
self.execute_task()
log.check(
(self.LOG_NAME, 'INFO', 'Course refund notification sent for refund %d.' % self.REFUND_ID),
)
......@@ -3,8 +3,10 @@
coverage==4.0.3
ddt==1.0.1
edx-lint==0.5.1
edx-lint==0.5.2
httpretty==0.8.10
mock==1.3.0
nose==1.3.7
nose-ignore-docstring==0.2
pep8==1.7.0
testfixtures==4.13.3
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment