add in chargeback handling (bug 695423)

This commit is contained in:
Andy McKay 2011-11-16 14:30:02 -08:00
Родитель df9b2ec3fd
Коммит fa4eadf9a7
6 изменённых файлов: 148 добавлений и 36 удалений

Просмотреть файл

@ -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': {