Commit 3f8f039d by Clinton Blackburn Committed by Clinton Blackburn

Sending notification after refund approval

An email is now sent to learners after a credit is issued for a refund. This functionality can be controlled for each site, and is only applicable for refunds of a course seat.

ECOM-6975
parent 4b72e75d
...@@ -106,6 +106,11 @@ class Command(BaseCommand): ...@@ -106,6 +106,11 @@ class Command(BaseCommand):
type=str, type=str,
required=False, required=False,
help='URL displayed to user for payment support') help='URL displayed to user for payment support')
parser.add_argument('--send-refund-notifications',
action='store_true',
dest='send_refund_notifications',
default=False,
help='Enable refund notification emails')
def handle(self, *args, **options): def handle(self, *args, **options):
site_id = options.get('site_id') site_id = options.get('site_id')
...@@ -149,6 +154,7 @@ class Command(BaseCommand): ...@@ -149,6 +154,7 @@ class Command(BaseCommand):
'segment_key': segment_key, 'segment_key': segment_key,
'from_email': from_email, 'from_email': from_email,
'enable_enrollment_codes': enable_enrollment_codes, 'enable_enrollment_codes': enable_enrollment_codes,
'send_refund_notifications': options['send_refund_notifications'],
'oauth_settings': { 'oauth_settings': {
'SOCIAL_AUTH_EDX_OIDC_URL_ROOT': '{lms_url_root}/oauth2'.format(lms_url_root=lms_url_root), 'SOCIAL_AUTH_EDX_OIDC_URL_ROOT': '{lms_url_root}/oauth2'.format(lms_url_root=lms_url_root),
'SOCIAL_AUTH_EDX_OIDC_KEY': client_id, 'SOCIAL_AUTH_EDX_OIDC_KEY': client_id,
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0022_auto_20161108_2101'),
]
operations = [
migrations.AddField(
model_name='siteconfiguration',
name='send_refund_notifications',
field=models.BooleanField(default=False, verbose_name='Send refund email notification'),
),
]
...@@ -126,6 +126,11 @@ class SiteConfiguration(models.Model): ...@@ -126,6 +126,11 @@ class SiteConfiguration(models.Model):
blank=True, blank=True,
default=False default=False
) )
send_refund_notifications = models.BooleanField(
verbose_name=_('Send refund email notification'),
blank=True,
default=False
)
class Meta(object): class Meta(object):
unique_together = ('site', 'partner') unique_together = ('site', 'partner')
......
...@@ -44,7 +44,7 @@ class CreateOrUpdateSiteCommandTests(TestCase): ...@@ -44,7 +44,7 @@ class CreateOrUpdateSiteCommandTests(TestCase):
def _call_command(self, site_domain, partner_code, lms_url_root, client_id, client_secret, from_email, def _call_command(self, site_domain, partner_code, lms_url_root, client_id, client_secret, from_email,
site_id=None, site_name=None, partner_name=None, theme_scss_path=None, site_id=None, site_name=None, partner_name=None, theme_scss_path=None,
payment_processors=None, segment_key=None, enable_enrollment_codes=False, payment_processors=None, segment_key=None, enable_enrollment_codes=False,
payment_support_email=None, payment_support_url=None): payment_support_email=None, payment_support_url=None, send_refund_notifications=False):
""" """
Internal helper method for interacting with the create_or_update_site management command Internal helper method for interacting with the create_or_update_site management command
""" """
...@@ -85,6 +85,10 @@ class CreateOrUpdateSiteCommandTests(TestCase): ...@@ -85,6 +85,10 @@ class CreateOrUpdateSiteCommandTests(TestCase):
command_args.append('--payment-support-url={payment_support_url}'.format( command_args.append('--payment-support-url={payment_support_url}'.format(
payment_support_url=payment_support_url payment_support_url=payment_support_url
)) ))
if send_refund_notifications:
command_args.append('--send-refund-notifications')
call_command(self.command_name, *command_args) call_command(self.command_name, *command_args)
def test_create_site(self): def test_create_site(self):
...@@ -108,6 +112,7 @@ class CreateOrUpdateSiteCommandTests(TestCase): ...@@ -108,6 +112,7 @@ class CreateOrUpdateSiteCommandTests(TestCase):
self._check_site_configuration(site, partner) self._check_site_configuration(site, partner)
self.assertFalse(site.siteconfiguration.enable_enrollment_codes) self.assertFalse(site.siteconfiguration.enable_enrollment_codes)
self.assertFalse(site.siteconfiguration.send_refund_notifications)
def test_update_site(self): def test_update_site(self):
""" Verify the command updates Site and creates Partner, and SiteConfiguration """ """ Verify the command updates Site and creates Partner, and SiteConfiguration """
...@@ -130,7 +135,8 @@ class CreateOrUpdateSiteCommandTests(TestCase): ...@@ -130,7 +135,8 @@ class CreateOrUpdateSiteCommandTests(TestCase):
from_email=self.from_email, from_email=self.from_email,
enable_enrollment_codes=True, enable_enrollment_codes=True,
payment_support_email=self.payment_support_email, payment_support_email=self.payment_support_email,
payment_support_url=self.payment_support_url payment_support_url=self.payment_support_url,
send_refund_notifications=True
) )
site = Site.objects.get(id=site.id) site = Site.objects.get(id=site.id)
...@@ -139,9 +145,12 @@ class CreateOrUpdateSiteCommandTests(TestCase): ...@@ -139,9 +145,12 @@ class CreateOrUpdateSiteCommandTests(TestCase):
self.assertEqual(site.domain, updated_site_domain) self.assertEqual(site.domain, updated_site_domain)
self.assertEqual(site.name, updated_site_name) self.assertEqual(site.name, updated_site_name)
self._check_site_configuration(site, partner) self._check_site_configuration(site, partner)
self.assertTrue(site.siteconfiguration.enable_enrollment_codes)
self.assertEqual(site.siteconfiguration.payment_support_email, self.payment_support_email) site_configuration = site.siteconfiguration
self.assertEqual(site.siteconfiguration.payment_support_url, self.payment_support_url) self.assertTrue(site_configuration.enable_enrollment_codes)
self.assertEqual(site_configuration.payment_support_email, self.payment_support_email)
self.assertEqual(site_configuration.payment_support_url, self.payment_support_url)
self.assertTrue(site_configuration.send_refund_notifications)
@data( @data(
['--site-id=1'], ['--site-id=1'],
......
...@@ -228,7 +228,7 @@ class RefundProcessViewTests(ThrottlingMixin, TestCase): ...@@ -228,7 +228,7 @@ class RefundProcessViewTests(ThrottlingMixin, TestCase):
refund = Refund.objects.get(id=self.refund.id) refund = Refund.objects.get(id=self.refund.id)
self.assertEqual(response.data['status'], refund.status) self.assertEqual(response.data['status'], refund.status)
self.assertEqual(response.data['status'], "Complete") self.assertEqual(response.data['status'], "Complete")
patched_log.info.assert_called_once_with( patched_log.info.assert_called_with(
"Skipping the revocation step for refund [%d].", self.refund.id) "Skipping the revocation step for refund [%d].", self.refund.id)
@ddt.data( @ddt.data(
......
import logging import logging
import urllib import urllib
from babel.numbers import format_currency from babel.numbers import format_currency as default_format_currency
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.translation import get_language, to_locale from django.utils.translation import get_language, to_locale
...@@ -9,7 +9,6 @@ from edx_rest_api_client.client import EdxRestApiClient ...@@ -9,7 +9,6 @@ from edx_rest_api_client.client import EdxRestApiClient
from requests.exceptions import ConnectionError, Timeout from requests.exceptions import ConnectionError, Timeout
from slumber.exceptions import SlumberHttpBaseException from slumber.exceptions import SlumberHttpBaseException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
...@@ -60,6 +59,18 @@ def get_receipt_page_url(site_configuration, order_number=None, override_url=Non ...@@ -60,6 +59,18 @@ def get_receipt_page_url(site_configuration, order_number=None, override_url=Non
) )
def format_currency(currency, amount, format=None, locale=None): # pylint: disable=redefined-builtin
locale = locale or to_locale(get_language())
format = format or getattr(settings, 'OSCAR_CURRENCY_FORMAT', None)
return default_format_currency(
amount,
currency,
format=format,
locale=locale
)
def add_currency(amount): def add_currency(amount):
""" Adds currency to the price amount. """ Adds currency to the price amount.
...@@ -69,9 +80,4 @@ def add_currency(amount): ...@@ -69,9 +80,4 @@ def add_currency(amount):
Returns: Returns:
str: Formatted price with currency. str: Formatted price with currency.
""" """
return format_currency( return format_currency(settings.OSCAR_DEFAULT_CURRENCY, amount, u'#,##0.00')
amount,
settings.OSCAR_DEFAULT_CURRENCY,
format=u'#,##0.00',
locale=to_locale(get_language())
)
from __future__ import unicode_literals
import logging import logging
from django.conf import settings from django.conf import settings
from django.db import models from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django_extensions.db.models import TimeStampedModel from django_extensions.db.models import TimeStampedModel
from ecommerce_worker.sailthru.v1.tasks import send_course_refund_email
from oscar.apps.payment.exceptions import PaymentError from oscar.apps.payment.exceptions import PaymentError
from oscar.core.loading import get_class, get_model from oscar.core.loading import get_class, get_model
from oscar.core.utils import get_default_currency from oscar.core.utils import get_default_currency
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
from ecommerce.core.constants import SEAT_PRODUCT_CLASS_NAME
from ecommerce.extensions.analytics.utils import audit_log from ecommerce.extensions.analytics.utils import audit_log
from ecommerce.extensions.checkout.utils import get_receipt_page_url, format_currency
from ecommerce.extensions.fulfillment.api import revoke_fulfillment_for_refund from ecommerce.extensions.fulfillment.api import revoke_fulfillment_for_refund
from ecommerce.extensions.order.constants import PaymentEventTypeName from ecommerce.extensions.order.constants import PaymentEventTypeName
from ecommerce.extensions.payment.helpers import get_processor_class_by_name from ecommerce.extensions.payment.helpers import get_processor_class_by_name
...@@ -186,6 +191,39 @@ class Refund(StatusMixin, TimeStampedModel): ...@@ -186,6 +191,39 @@ class Refund(StatusMixin, TimeStampedModel):
# This occurs when attempting to refund free orders. # This occurs when attempting to refund free orders.
logger.info("No payments to credit for Refund [%d]", self.id) logger.info("No payments to credit for Refund [%d]", self.id)
def _notify_purchaser(self):
""" Notify the purchaser that the refund has been processed. """
site_configuration = self.order.site.siteconfiguration
site_code = site_configuration.partner.short_code
if not site_configuration.send_refund_notifications:
logger.info(
'Refund notifications are disabled for Partner [%s]. No notification will be sent for Refund [%d]',
site_code, self.id
)
return
# NOTE (CCB): The initial version of the refund email only supports refunding a single course.
product = self.lines.first().order_line.product
product_class = product.get_product_class().name
if product_class != SEAT_PRODUCT_CLASS_NAME:
logger.warning(
('No refund notification will be sent for Refund [%d]. The notification supports product lines '
'of type Course, not [%s].'),
self.id, product_class
)
return
course_name = self.lines.first().order_line.product.course.name
order_number = self.order.number
order_url = get_receipt_page_url(site_configuration, order_number)
amount = format_currency(self.currency, self.total_credit_excl_tax)
send_course_refund_email.delay(self.user.email, self.id, amount, course_name, order_number,
order_url, site_code=site_code)
logger.info('Course refund notification scheduled for Refund [%d].', self.id)
def _revoke_lines(self): def _revoke_lines(self):
"""Revoke fulfillment for the lines in this Refund.""" """Revoke fulfillment for the lines in this Refund."""
if revoke_fulfillment_for_refund(self): if revoke_fulfillment_for_refund(self):
...@@ -202,6 +240,7 @@ class Refund(StatusMixin, TimeStampedModel): ...@@ -202,6 +240,7 @@ class Refund(StatusMixin, TimeStampedModel):
try: try:
self._issue_credit() self._issue_credit()
self.set_status(REFUND.PAYMENT_REFUNDED) self.set_status(REFUND.PAYMENT_REFUNDED)
self._notify_purchaser()
except PaymentError: except PaymentError:
logger.exception('Failed to issue credit for refund [%d].', self.id) logger.exception('Failed to issue credit for refund [%d].', self.id)
self.set_status(REFUND.PAYMENT_REFUND_ERROR) self.set_status(REFUND.PAYMENT_REFUND_ERROR)
......
...@@ -70,8 +70,8 @@ class RefundTestMixin(CourseCatalogTestMixin): ...@@ -70,8 +70,8 @@ class RefundTestMixin(CourseCatalogTestMixin):
self.assertEqual(refund_line.line_credit_excl_tax, order_line.line_price_excl_tax) self.assertEqual(refund_line.line_credit_excl_tax, order_line.line_price_excl_tax)
self.assertEqual(refund_line.quantity, order_line.quantity) self.assertEqual(refund_line.quantity, order_line.quantity)
def create_refund(self, processor_name=DummyProcessor.NAME): def create_refund(self, processor_name=DummyProcessor.NAME, **kwargs):
refund = RefundFactory() refund = RefundFactory(**kwargs)
order = refund.order order = refund.order
source_type, __ = SourceType.objects.get_or_create(name=processor_name) source_type, __ = SourceType.objects.get_or_create(name=processor_name)
Source.objects.create(source_type=source_type, order=order, currency=refund.currency, Source.objects.create(source_type=source_type, order=order, currency=refund.currency,
......
from __future__ import unicode_literals
from decimal import Decimal
import ddt import ddt
import httpretty import httpretty
import mock import mock
from django.conf import settings from django.conf import settings
from oscar.apps.payment.exceptions import PaymentError from oscar.apps.payment.exceptions import PaymentError
from oscar.core.loading import get_model, get_class from oscar.core.loading import get_model, get_class
from oscar.test.factories import create_basket
from oscar.test.newfactories import UserFactory from oscar.test.newfactories import UserFactory
from testfixtures import LogCapture from testfixtures import LogCapture
from ecommerce.core.constants import SEAT_PRODUCT_CLASS_NAME
from ecommerce.core.url_utils import get_lms_enrollment_api_url from ecommerce.core.url_utils import get_lms_enrollment_api_url
from ecommerce.courses.tests.factories import CourseFactory
from ecommerce.extensions.checkout.utils import get_receipt_page_url, format_currency
from ecommerce.extensions.payment.tests.processors import DummyProcessor from ecommerce.extensions.payment.tests.processors import DummyProcessor
from ecommerce.extensions.refund import models from ecommerce.extensions.refund import models
from ecommerce.extensions.refund.exceptions import InvalidStatus from ecommerce.extensions.refund.exceptions import InvalidStatus
from ecommerce.extensions.refund.status import REFUND, REFUND_LINE from ecommerce.extensions.refund.status import REFUND, REFUND_LINE
from ecommerce.extensions.refund.tests.factories import RefundFactory, RefundLineFactory from ecommerce.extensions.refund.tests.factories import RefundFactory, RefundLineFactory
from ecommerce.extensions.refund.tests.mixins import RefundTestMixin from ecommerce.extensions.refund.tests.mixins import RefundTestMixin
from ecommerce.extensions.test.factories import create_order
from ecommerce.tests.testcases import TestCase from ecommerce.tests.testcases import TestCase
PaymentEventType = get_model('order', 'PaymentEventType') PaymentEventType = get_model('order', 'PaymentEventType')
...@@ -22,6 +31,7 @@ Refund = get_model('refund', 'Refund') ...@@ -22,6 +31,7 @@ Refund = get_model('refund', 'Refund')
Source = get_model('payment', 'Source') Source = get_model('payment', 'Source')
LOGGER_NAME = 'ecommerce.extensions.analytics.utils' LOGGER_NAME = 'ecommerce.extensions.analytics.utils'
REFUND_MODEL_LOGGER_NAME = 'ecommerce.extensions.refund.models'
class StatusTestsMixin(object): class StatusTestsMixin(object):
...@@ -213,10 +223,14 @@ class RefundTests(RefundTestMixin, StatusTestsMixin, TestCase): ...@@ -213,10 +223,14 @@ class RefundTests(RefundTestMixin, StatusTestsMixin, TestCase):
RefundLine objects to Complete, and return True. RefundLine objects to Complete, and return True.
""" """
self.site.siteconfiguration.segment_key = None self.site.siteconfiguration.segment_key = None
self.site.siteconfiguration.send_refund_notifications = True
refund = self.create_refund() refund = self.create_refund()
source = refund.order.sources.first() source = refund.order.sources.first()
with LogCapture(LOGGER_NAME) as l: with LogCapture(LOGGER_NAME) as l:
self.approve(refund) with mock.patch.object(Refund, '_notify_purchaser', return_value=None) as mock_notify:
self.approve(refund)
l.check( l.check(
( (
...@@ -243,6 +257,9 @@ class RefundTests(RefundTestMixin, StatusTestsMixin, TestCase): ...@@ -243,6 +257,9 @@ class RefundTests(RefundTestMixin, StatusTestsMixin, TestCase):
self.assert_valid_payment_event_fields(payment_event, refund.total_credit_excl_tax, paid_type, self.assert_valid_payment_event_fields(payment_event, refund.total_credit_excl_tax, paid_type,
DummyProcessor.NAME, DummyProcessor.REFUND_TRANSACTION_ID) DummyProcessor.NAME, DummyProcessor.REFUND_TRANSACTION_ID)
# Verify an attempt is made to send a notification
mock_notify.assert_called_once_with()
def test_approve_payment_error(self): def test_approve_payment_error(self):
""" """
If payment refund fails, the Refund status should be set to Payment Refund Error, and the RefundLine If payment refund fails, the Refund status should be set to Payment Refund Error, and the RefundLine
...@@ -309,11 +326,10 @@ class RefundTests(RefundTestMixin, StatusTestsMixin, TestCase): ...@@ -309,11 +326,10 @@ class RefundTests(RefundTestMixin, StatusTestsMixin, TestCase):
# Make RefundLine.deny() raise an exception # Make RefundLine.deny() raise an exception
with mock.patch('ecommerce.extensions.refund.models.RefundLine.deny', side_effect=Exception): with mock.patch('ecommerce.extensions.refund.models.RefundLine.deny', side_effect=Exception):
logger_name = 'ecommerce.extensions.refund.models' with LogCapture(REFUND_MODEL_LOGGER_NAME) as l:
with LogCapture(logger_name) as l:
self.assertFalse(refund.deny()) self.assertFalse(refund.deny())
l.check((logger_name, 'ERROR', 'Failed to deny RefundLine [{}].'.format(refund.lines.first().id))) l.check((REFUND_MODEL_LOGGER_NAME, 'ERROR',
'Failed to deny RefundLine [{}].'.format(refund.lines.first().id)))
@ddt.data(REFUND.REVOCATION_ERROR, REFUND.PAYMENT_REFUNDED, REFUND.PAYMENT_REFUND_ERROR, REFUND.COMPLETE) @ddt.data(REFUND.REVOCATION_ERROR, REFUND.PAYMENT_REFUNDED, REFUND.PAYMENT_REFUND_ERROR, REFUND.COMPLETE)
def test_deny_wrong_state(self, status): def test_deny_wrong_state(self, status):
...@@ -326,6 +342,80 @@ class RefundTests(RefundTestMixin, StatusTestsMixin, TestCase): ...@@ -326,6 +342,80 @@ class RefundTests(RefundTestMixin, StatusTestsMixin, TestCase):
self.assertEqual(refund.status, status) self.assertEqual(refund.status, status)
self.assert_line_status(refund, REFUND_LINE.OPEN) self.assert_line_status(refund, REFUND_LINE.OPEN)
@mock.patch('ecommerce_worker.sailthru.v1.tasks.send_course_refund_email.delay')
def test_notify_purchaser(self, mock_task):
""" Verify the notification is scheduled if the site has notifications enabled
and the refund is for a course seat.
"""
site_configuration = self.site.siteconfiguration
site_configuration.send_refund_notifications = True
user = UserFactory()
course = CourseFactory()
price = Decimal(100.00)
product = course.create_or_update_seat('verified', True, price, self.partner)
basket = create_basket(empty=True)
basket.site = self.site
basket.add_product(product)
order = create_order(basket=basket, user=user)
order_url = get_receipt_page_url(site_configuration, order.number)
refund = Refund.create_with_lines(order, order.lines.all())
with LogCapture(REFUND_MODEL_LOGGER_NAME) as l:
refund._notify_purchaser() # pylint: disable=protected-access
msg = 'Course refund notification scheduled for Refund [{}].'.format(refund.id)
l.check(
(REFUND_MODEL_LOGGER_NAME, 'INFO', msg)
)
amount = format_currency(order.currency, price)
mock_task.assert_called_once_with(
user.email, refund.id, amount, course.name, order.number, order_url, site_code=self.partner.short_code
)
@mock.patch('ecommerce_worker.sailthru.v1.tasks.send_course_refund_email.delay')
def test_notify_purchaser_with_notifications_disabled(self, mock_task):
""" Verify no notification is sent if the functionality is disabled for the site. """
self.site.siteconfiguration.send_refund_notifications = False
order = create_order(site=self.site)
refund = self.create_refund(order=order)
with LogCapture(REFUND_MODEL_LOGGER_NAME) as l:
refund._notify_purchaser() # pylint: disable=protected-access
msg = 'Refund notifications are disabled for Partner [{code}]. ' \
'No notification will be sent for Refund [{id}]'.format(code=self.partner.short_code, id=refund.id)
l.check(
(REFUND_MODEL_LOGGER_NAME, 'INFO', msg)
)
self.assertFalse(mock_task.called)
@mock.patch('ecommerce_worker.sailthru.v1.tasks.send_course_refund_email.delay')
def test_notify_purchaser_without_course_product_class(self, mock_task):
""" Verify a notification is not sent if the refunded item is not a course seat. """
self.site.siteconfiguration.send_refund_notifications = True
order = create_order(site=self.site)
product_class = order.lines.first().product.get_product_class().name
self.assertNotEqual(product_class, SEAT_PRODUCT_CLASS_NAME)
refund = self.create_refund(order=order)
with LogCapture(REFUND_MODEL_LOGGER_NAME) as l:
refund._notify_purchaser() # pylint: disable=protected-access
msg = ('No refund notification will be sent for Refund [{id}]. The notification supports product '
'lines of type Course, not [{product_class}].').format(product_class=product_class, id=refund.id)
l.check(
(REFUND_MODEL_LOGGER_NAME, 'WARNING', msg)
)
self.assertFalse(mock_task.called)
class RefundLineTests(StatusTestsMixin, TestCase): class RefundLineTests(StatusTestsMixin, TestCase):
pipeline = settings.OSCAR_REFUND_LINE_STATUS_PIPELINE pipeline = settings.OSCAR_REFUND_LINE_STATUS_PIPELINE
......
...@@ -28,6 +28,7 @@ class SiteConfigurationFactory(factory.DjangoModelFactory): ...@@ -28,6 +28,7 @@ class SiteConfigurationFactory(factory.DjangoModelFactory):
lms_url_root = factory.LazyAttribute(lambda obj: "http://lms.testserver.fake") lms_url_root = factory.LazyAttribute(lambda obj: "http://lms.testserver.fake")
site = factory.SubFactory(SiteFactory) site = factory.SubFactory(SiteFactory)
partner = factory.SubFactory(PartnerFactory) partner = factory.SubFactory(PartnerFactory)
send_refund_notifications = False
class StockRecordFactory(OscarStockRecordFactory): class StockRecordFactory(OscarStockRecordFactory):
......
...@@ -19,7 +19,7 @@ edx-auth-backends==0.6.0 ...@@ -19,7 +19,7 @@ edx-auth-backends==0.6.0
edx-django-release-util==0.2.0 edx-django-release-util==0.2.0
edx-django-sites-extensions==1.0.0 edx-django-sites-extensions==1.0.0
edx-drf-extensions==1.2.2 edx-drf-extensions==1.2.2
edx-ecommerce-worker==0.5.0 edx-ecommerce-worker==0.6.0
edx-opaque-keys==0.3.1 edx-opaque-keys==0.3.1
edx-rest-api-client==1.6.0 edx-rest-api-client==1.6.0
jsonfield==1.0.3 jsonfield==1.0.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