add in chargeback handling (bug 695423)
This commit is contained in:
Родитель
df9b2ec3fd
Коммит
fa4eadf9a7
|
@ -116,6 +116,7 @@ class TestPaypal(amo.tests.TestCase):
|
|||
@patch('paypal.views.urllib2.urlopen')
|
||||
class TestEmbeddedPaymentsPaypal(amo.tests.TestCase):
|
||||
fixtures = ['base/users', 'base/addon_3615']
|
||||
uuid = 'e76059abcf747f5b4e838bf47822e6b2'
|
||||
|
||||
def setUp(self):
|
||||
self.url = reverse('amo.paypal')
|
||||
|
@ -127,31 +128,29 @@ class TestEmbeddedPaymentsPaypal(amo.tests.TestCase):
|
|||
return m
|
||||
|
||||
def test_success(self, urlopen):
|
||||
uuid = 'e76059abcf747f5b4e838bf47822e6b2'
|
||||
Contribution.objects.create(uuid=uuid, addon=self.addon)
|
||||
data = {'tracking_id': uuid, 'payment_status': 'Completed'}
|
||||
Contribution.objects.create(uuid=self.uuid, addon=self.addon)
|
||||
data = {'tracking_id': self.uuid, 'payment_status': 'Completed'}
|
||||
urlopen.return_value = self.urlopener('VERIFIED')
|
||||
|
||||
response = self.client.post(self.url, data)
|
||||
eq_(response.content, 'Success!')
|
||||
|
||||
def test_wrong_uuid(self, urlopen):
|
||||
uuid = 'e76059abcf747f5b4e838bf47822e6b2'
|
||||
Contribution.objects.create(uuid=uuid, addon=self.addon)
|
||||
Contribution.objects.create(uuid=self.uuid, addon=self.addon)
|
||||
data = {'tracking_id': 'sdf', 'payment_status': 'Completed'}
|
||||
urlopen.return_value = self.urlopener('VERIFIED')
|
||||
|
||||
response = self.client.post(self.url, data)
|
||||
eq_(response.content, 'Contribution not found')
|
||||
|
||||
def _receive_refund_ipn(self, uuid, urlopen):
|
||||
def _receive_ipn(self, uuid, urlopen, action):
|
||||
"""
|
||||
Create and post a refund IPN.
|
||||
Create and post an IPN.
|
||||
"""
|
||||
urlopen.return_value = self.urlopener('VERIFIED')
|
||||
response = self.client.post(self.url, {u'action_type': u'PAY',
|
||||
u'sender_email': u'a@a.com',
|
||||
u'status': u'REFUNDED',
|
||||
u'status': action,
|
||||
u'tracking_id': u'123',
|
||||
u'mc_gross': u'12.34',
|
||||
u'mc_currency': u'US',
|
||||
|
@ -163,12 +162,11 @@ class TestEmbeddedPaymentsPaypal(amo.tests.TestCase):
|
|||
Receipt of an IPN for a refund results in a Contribution
|
||||
object recording its relation to the original payment.
|
||||
"""
|
||||
uuid = 'e76059abcf747f5b4e838bf47822e6b2'
|
||||
user = UserProfile.objects.get(pk=999)
|
||||
original = Contribution.objects.create(uuid=uuid, user=user,
|
||||
original = Contribution.objects.create(uuid=self.uuid, user=user,
|
||||
addon=self.addon)
|
||||
|
||||
response = self._receive_refund_ipn(uuid, urlopen)
|
||||
response = self._receive_ipn(self.uuid, urlopen, 'Refunded')
|
||||
eq_(response.content, 'Success!')
|
||||
refunds = Contribution.objects.filter(related=original)
|
||||
eq_(len(refunds), 1)
|
||||
|
@ -182,8 +180,27 @@ class TestEmbeddedPaymentsPaypal(amo.tests.TestCase):
|
|||
Receipt of an IPN for a refund for a payment we haven't
|
||||
recorded results in an error.
|
||||
"""
|
||||
uuid = 'e76059abcf747f5b4e838bf47822e6b2'
|
||||
response = self._receive_refund_ipn(uuid, urlopen)
|
||||
response = self._receive_ipn(self.uuid, urlopen, 'Refunded')
|
||||
eq_(response.content, 'Contribution not found')
|
||||
refunds = Contribution.objects.filter(type=amo.CONTRIB_REFUND)
|
||||
eq_(len(refunds), 0)
|
||||
|
||||
def reversal(self, urlopen):
|
||||
user = UserProfile.objects.get(pk=999)
|
||||
Contribution.objects.create(uuid=self.uuid, user=user,
|
||||
addon=self.addon)
|
||||
response = self._receive_ipn(self.uuid, urlopen, 'Reversal')
|
||||
eq_(response.content, 'Success!')
|
||||
|
||||
def test_related(self, urlopen):
|
||||
self.reversal(urlopen)
|
||||
eq_(Contribution.objects.all()[1].related.pk,
|
||||
Contribution.objects.all()[0].pk)
|
||||
|
||||
def test_chargeback(self, urlopen):
|
||||
self.reversal(urlopen)
|
||||
eq_(Contribution.objects.all()[1].type, amo.CONTRIB_CHARGEBACK)
|
||||
|
||||
def test_email(self, urlopen):
|
||||
self.reversal(urlopen)
|
||||
eq_(len(mail.outbox), 1)
|
||||
|
|
|
@ -72,7 +72,6 @@ def _paypal(request):
|
|||
with statsd.timer('paypal.validate-ipn'):
|
||||
paypal_response = urllib2.urlopen(settings.PAYPAL_CGI_URL,
|
||||
data, 20).readline()
|
||||
|
||||
# List of (old, new) codes so we can transpose the data for
|
||||
# embedded payments.
|
||||
for old, new in [('payment_status', 'status'),
|
||||
|
@ -94,7 +93,10 @@ def _paypal(request):
|
|||
return http.HttpResponse('Success!')
|
||||
|
||||
payment_status = post.get('payment_status', '').lower()
|
||||
if payment_status not in ('refunded', 'completed'):
|
||||
methods = {'refunded': paypal_refunded,
|
||||
'completed': paypal_completed,
|
||||
'reversal': paypal_reversal}
|
||||
if payment_status not in methods:
|
||||
# Skip processing for anything other than events that change
|
||||
# payment status.
|
||||
paypal_log.info('Ignoring: %s, %s' %
|
||||
|
@ -125,22 +127,20 @@ def _paypal(request):
|
|||
cache.set(key, count, 1209600) # This is 2 weeks.
|
||||
return http.HttpResponseServerError('Contribution not found')
|
||||
|
||||
if payment_status == 'refunded':
|
||||
paypal_log.info('Calling refunded: %s' % post.get('txn_id', ''))
|
||||
return paypal_refunded(request, post, c)
|
||||
elif payment_status == 'completed':
|
||||
paypal_log.info('Calling completed: %s' % post.get('txn_id', ''))
|
||||
return paypal_completed(request, post, c)
|
||||
paypal_log.info('Calling %s: %s' % (payment_status,
|
||||
post.get('txn_id', '')))
|
||||
return methods[payment_status](request, post, c)
|
||||
|
||||
|
||||
def paypal_refunded(request, post, original):
|
||||
|
||||
# Make sure transaction has not yet been processed.
|
||||
if (Contribution.objects
|
||||
.filter(transaction_id=post['txn_id'],
|
||||
type=amo.CONTRIB_REFUND).count()) > 0:
|
||||
if (Contribution.objects.filter(transaction_id=post['txn_id'],
|
||||
type=amo.CONTRIB_REFUND)
|
||||
.exists()):
|
||||
paypal_log.info('Refund IPN already processed')
|
||||
return http.HttpResponse('Transaction already processed')
|
||||
paypal_log.info('Refund IPN received for transaction %s' % post['txn_id'])
|
||||
|
||||
paypal_log.info('Refund IPN received: %s' % post['txn_id'])
|
||||
refund = Contribution.objects.create(
|
||||
addon=original.addon, related=original,
|
||||
user=original.user, type=amo.CONTRIB_REFUND,
|
||||
|
@ -150,15 +150,38 @@ def paypal_refunded(request, post, original):
|
|||
refund.uuid = None
|
||||
refund.post_data = php.serialize(post)
|
||||
refund.save()
|
||||
# TODO: Send the refund email to let them know.
|
||||
return http.HttpResponse('Success!')
|
||||
|
||||
|
||||
def paypal_reversal(request, post, original):
|
||||
if (Contribution.objects.filter(transaction_id=post['txn_id'],
|
||||
type=amo.CONTRIB_CHARGEBACK)
|
||||
.exists()):
|
||||
paypal_log.info('Reversal IPN already processed')
|
||||
return http.HttpResponse('Transaction already processed')
|
||||
|
||||
paypal_log.info('Reversal IPN received: %s' % post['txn_id'])
|
||||
refund = Contribution.objects.create(
|
||||
addon=original.addon, related=original,
|
||||
user=original.user, type=amo.CONTRIB_CHARGEBACK,
|
||||
amount=post['mc_gross'], currency=post['mc_currency'],
|
||||
post_data=php.serialize(post)
|
||||
)
|
||||
# Send an email to the end user.
|
||||
refund.mail_chargeback()
|
||||
return http.HttpResponse('Success!')
|
||||
|
||||
|
||||
def paypal_completed(request, post, c):
|
||||
# Make sure transaction has not yet been processed.
|
||||
if (Contribution.objects
|
||||
.filter(transaction_id=post['txn_id'],
|
||||
type=amo.CONTRIB_PURCHASE).count()) > 0:
|
||||
if (Contribution.objects.filter(transaction_id=post['txn_id'],
|
||||
type=amo.CONTRIB_PURCHASE)
|
||||
.exists()):
|
||||
paypal_log.info('Completed IPN already processed')
|
||||
return http.HttpResponse('Transaction already processed')
|
||||
|
||||
paypal_log.info('Completed IPN received: %s' % post['txn_id'])
|
||||
c.transaction_id = post['txn_id']
|
||||
# Embedded payments does not send an mc_gross.
|
||||
if 'mc_gross' in post:
|
||||
|
|
|
@ -7,6 +7,7 @@ from django.utils import translation
|
|||
|
||||
from babel import Locale, numbers
|
||||
import caching.base
|
||||
from jingo import env
|
||||
from jinja2.filters import do_dictsort
|
||||
import tower
|
||||
from tower import ugettext as _
|
||||
|
@ -180,6 +181,31 @@ class Contribution(models.Model):
|
|||
# post_data may be None or missing a key
|
||||
return None
|
||||
|
||||
def _switch_locale(self):
|
||||
if self.source_locale:
|
||||
lang = self.source_locale
|
||||
else:
|
||||
lang = self.addon.default_locale
|
||||
tower.activate(lang)
|
||||
return Locale(translation.to_locale(lang))
|
||||
|
||||
|
||||
def mail_chargeback(self):
|
||||
"""
|
||||
Send a mail to the purchaser of an add-on about the reversal from
|
||||
Paypal.
|
||||
"""
|
||||
locale = self._switch_locale()
|
||||
t = env.get_template('users/support/emails/chargeback.txt')
|
||||
amt = numbers.format_currency(abs(self.amount), self.currency,
|
||||
locale=locale)
|
||||
body = t.render({'name': self.addon.name, 'amount': amt})
|
||||
# L10n: the adddon name.
|
||||
subject = _(u'%s payment reversal' % self.addon.name)
|
||||
send_mail(subject, body, settings.MARKETPLACE_EMAIL,
|
||||
[self.user.email], fail_silently=True)
|
||||
|
||||
|
||||
def mail_thankyou(self, request=None):
|
||||
"""
|
||||
Mail a thankyou note for a completed contribution.
|
||||
|
@ -187,13 +213,7 @@ class Contribution(models.Model):
|
|||
Raises a ``ContributionError`` exception when the contribution
|
||||
is not complete or email addresses are not found.
|
||||
"""
|
||||
|
||||
# Setup l10n before loading addon.
|
||||
if self.source_locale:
|
||||
lang = self.source_locale
|
||||
else:
|
||||
lang = self.addon.default_locale
|
||||
tower.activate(lang)
|
||||
self._switch_locale()
|
||||
|
||||
# Thankyous must be enabled.
|
||||
if not self.addon.enable_thankyou:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import json
|
||||
|
||||
from django.core import mail
|
||||
from django.db import models
|
||||
from django.utils import translation
|
||||
|
||||
|
@ -46,3 +48,47 @@ class TestContributionModel(amo.tests.TestCase):
|
|||
eq_(Contribution.objects.all()[0].get_amount_locale(), u'$1.99')
|
||||
translation.activate('fr')
|
||||
eq_(Contribution.objects.all()[0].get_amount_locale(), u'1,99\xa0$US')
|
||||
|
||||
|
||||
class TestEmail(amo.tests.TestCase):
|
||||
fixtures = ['base/users', 'base/addon_3615']
|
||||
|
||||
def setUp(self):
|
||||
self.addon = Addon.objects.get(pk=3615)
|
||||
self.user = UserProfile.objects.get(pk=999)
|
||||
|
||||
def chargeback_email(self, amount, locale):
|
||||
cont = Contribution.objects.create(type=amo.CONTRIB_CHARGEBACK,
|
||||
addon=self.addon, user=self.user,
|
||||
amount=amount,
|
||||
source_locale=locale)
|
||||
cont.mail_chargeback()
|
||||
eq_(len(mail.outbox), 1)
|
||||
return mail.outbox[0]
|
||||
|
||||
def test_chargeback_email(self):
|
||||
email = self.chargeback_email('10', 'en-US')
|
||||
eq_(email.subject, u'%s payment reversal' % self.addon.name)
|
||||
assert str(self.addon.name) in email.body
|
||||
|
||||
def test_chargeback_negative(self):
|
||||
email = self.chargeback_email('-10', 'en-US')
|
||||
assert '$10.00' in email.body
|
||||
|
||||
def test_chargeback_positive(self):
|
||||
email = self.chargeback_email('10', 'en-US')
|
||||
assert '$10.00' in email.body
|
||||
|
||||
def test_chargeback_unicode(self):
|
||||
self.addon.name = u'Азәрбајҹан'
|
||||
self.addon.save()
|
||||
email = self.chargeback_email('-10', 'en-US')
|
||||
assert '$10.00' in email.body
|
||||
|
||||
def test_chargeback_locale(self):
|
||||
self.addon.name = {'fr':u'België'}
|
||||
self.addon.locale = 'fr'
|
||||
self.addon.save()
|
||||
email = self.chargeback_email('-10', 'fr')
|
||||
assert u'België' in email.body
|
||||
assert u'10,00\xa0$US' in email.body
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{% trans %}
|
||||
A reversal has occurred for your payment for {{ name }} from PayPal. Your account has been refunded {{ amount }}.
|
||||
|
||||
Thank you for shopping in the Mozilla Marketplace.
|
||||
{% endtrans %}
|
|
@ -50,6 +50,7 @@ MANAGERS = ADMINS
|
|||
FLIGTAR = 'amo-admins@mozilla.org'
|
||||
EDITORS_EMAIL = 'amo-editors@mozilla.org'
|
||||
SENIOR_EDITORS_EMAIL = 'amo-admin-reviews@mozilla.org'
|
||||
MARKETPLACE_EMAIL = 'amo-marketplace@mozilla.org'
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
|
|
Загрузка…
Ссылка в новой задаче