add in preapproval tokens if present, add in to purchase flow and make sure contributions are compat. (bug 707371, 707373)

This commit is contained in:
Andy McKay 2011-12-15 15:26:30 -08:00
Родитель af272542cb
Коммит 966ba81f4c
7 изменённых файлов: 229 добавлений и 43 удалений

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

@ -1,5 +1,5 @@
{# these are custom attributes on <div class="install ..."> found in
# apps/addons/templates/addons/impala/button.html and
{# these are custom attributes on <div class="install ..."> found in
# apps/addons/templates/addons/impala/button.html and
# apps/addons/templates/addons/mobile/button.html #}
data-addon="{{ addon.id }}"
data-icon="{{ addon.icon_url }}"

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

@ -45,7 +45,8 @@
</div>
<button class="button prominent paypal"
href="{{ shared_url('addons.purchase', addon) }}?"
data-realurl="{{ download }}">
data-realurl="{{ download }}"
data-thanksurl="{{ shared_url('addons.purchase.thanks', addon) }}">
{# The <small> makes it smaller, <em> makes it darker. Don't localize "PayPal". #}
{{ loc('Pay <small>with</small> Pay<em>Pal</em>')|xssafe }}
</button>

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

@ -25,7 +25,7 @@ from abuse.models import AbuseReport
from addons.models import (Addon, AddonDependency, AddonUpsell, AddonUser,
Charity)
from files.models import File
from market.models import AddonPremium, AddonPurchase, Price
from market.models import AddonPremium, AddonPurchase, PreApprovalUser, Price
from paypal.tests.test import other_error
from stats.models import Contribution
from translations.helpers import truncate
@ -151,7 +151,7 @@ class TestContributeEmbedded(amo.tests.TestCase):
@patch('paypal.get_paykey')
def client_get(self, get_paykey, **kwargs):
get_paykey.return_value = 'abc'
get_paykey.return_value = ['abc', '']
url = reverse('addons.contribute', args=kwargs.pop('rev'))
if 'qs' in kwargs:
url = url + kwargs.pop('qs')
@ -296,20 +296,20 @@ class TestPurchaseEmbedded(amo.tests.TestCase):
@patch('paypal.get_paykey')
def test_redirect(self, get_paykey):
get_paykey.return_value = 'some-pay-key'
get_paykey.return_value = ['some-pay-key', '']
res = self.client.get(self.purchase_url)
assert 'some-pay-key' in res['Location']
@patch('paypal.get_paykey')
def test_ajax(self, get_paykey):
get_paykey.return_value = 'some-pay-key'
get_paykey.return_value = ['some-pay-key', '']
res = self.client.get_ajax(self.purchase_url)
assert json.loads(res.content)['paykey'] == 'some-pay-key'
eq_(json.loads(res.content)['paykey'], 'some-pay-key')
@patch('paypal.get_paykey')
def test_paykey_amount(self, get_paykey):
# Test the amount the paykey for is the price.
get_paykey.return_value = 'some-pay-key'
get_paykey.return_value = ['some-pay-key', '']
self.client.get_ajax(self.purchase_url)
# wtf? Can we get any more [0]'s there?
eq_(get_paykey.call_args_list[0][0][0]['amount'], Decimal('0.99'))
@ -322,12 +322,54 @@ class TestPurchaseEmbedded(amo.tests.TestCase):
@patch('paypal.get_paykey')
def test_paykey_contribution(self, get_paykey):
get_paykey.return_value = 'some-pay-key'
get_paykey.return_value = ['some-pay-key', '']
self.client.get_ajax(self.purchase_url)
cons = Contribution.objects.filter(type=amo.CONTRIB_PENDING)
eq_(cons.count(), 1)
eq_(cons[0].amount, Decimal('0.99'))
def check_contribution(self, state):
cons = Contribution.objects.all()
eq_(cons.count(), 1)
eq_(cons[0].type, state)
@patch('paypal.check_purchase')
@patch('paypal.get_paykey')
def get_with_preapproval(self, get_paykey, check_purchase,
check_purchase_result=None):
get_paykey.return_value = ['some-pay-key', 'COMPLETED']
check_purchase.return_value = check_purchase_result
return self.client.get_ajax(self.purchase_url)
def test_paykey_pre_approval(self):
res = self.get_with_preapproval(check_purchase_result='COMPLETED')
eq_(json.loads(res.content)['status'], 'COMPLETED')
self.check_contribution(amo.CONTRIB_PURCHASE)
def test_paykey_pre_approval_disagree(self):
res = self.get_with_preapproval(check_purchase_result='No!!!')
eq_(json.loads(res.content)['status'], 'NOT-COMPLETED')
self.check_contribution(amo.CONTRIB_PENDING)
@patch('paypal.check_purchase')
@patch('paypal.get_paykey')
def test_paykey_pre_approval_no_ajax(self, get_paykey, check_purchase):
get_paykey.return_value = ['some-pay-key', 'COMPLETED']
check_purchase.return_value = 'COMPLETED'
res = self.client.get(self.purchase_url)
self.assertRedirects(res, shared_url('addons.detail', self.addon))
@patch('paypal.check_purchase')
@patch('paypal.get_paykey')
# Turning on the allow-pre-auth flag.
@patch.object(waffle, 'flag_is_active', lambda x, y: True)
def test_paykey_pre_approval_used(self, get_paykey, check_purchase):
get_paykey.return_value = ['some-pay-key', 'COMPLETED']
check_purchase.return_value = 'COMPLETED'
pre = PreApprovalUser.objects.create(user=self.user, paypal_key='xyz')
self.client.get_ajax(self.purchase_url)
eq_(get_paykey.call_args[0][0]['preapproval'], pre)
@patch('addons.models.Addon.has_purchased')
def test_has_purchased(self, has_purchased):
has_purchased.return_value = True
@ -548,6 +590,13 @@ class TestPaypalStart(PaypalStart):
self.test_loggedin_notpurchased()
eq_(Installed.objects.count(), 0)
def test_has_thanksurl(self):
assert self.client.login(**self.data)
res = self.client.get_ajax(self.url)
eq_(pq(res.content).find('button.paypal').attr('data-thanksurl'),
shared_url('addons.purchase.thanks', self.addon))
@patch.object(waffle, 'switch_is_active', lambda x: True)
@patch.object(settings, 'LOGIN_RATELIMIT_USER', 10)

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

@ -20,12 +20,14 @@ import jinja2
import commonware.log
import session_csrf
from tower import ugettext as _, ugettext_lazy as _lazy
import waffle
from mobility.decorators import mobilized, mobile_template
import amo
from amo import messages
from amo.decorators import login_required, write
from amo.forms import AbuseForm
from amo.helpers import shared_url
from amo.utils import sorted_groupby, randslice
from amo.models import manual_order
from amo import urlresolvers
@ -499,8 +501,12 @@ def purchase(request, addon):
uuid_ = hashlib.md5(str(uuid.uuid4())).hexdigest()
# l10n: {0} is the addon name
contrib_for = _(u'Purchase of {0}').format(jinja2.escape(addon.name))
paykey, status, error = '', '', ''
preapproval = None
if waffle.flag_is_active(request, 'allow-pre-auth') and request.amo_user:
preapproval = request.amo_user.get_preapproval()
paykey, error = '', ''
try:
pattern = 'addons.purchase.finished'
slug = addon.slug
@ -508,12 +514,17 @@ def purchase(request, addon):
pattern = 'apps.purchase.finished'
slug = addon.app_slug
paykey = paypal.get_paykey(dict(uuid=uuid_, slug=slug,
amount=amount, memo=contrib_for, email=addon.paypal_id,
paykey, status = paypal.get_paykey(dict(
amount=amount,
chains=settings.PAYPAL_CHAINS,
email=addon.paypal_id,
ip=request.META.get('REMOTE_ADDR'),
memo=contrib_for,
pattern=pattern,
preapproval=preapproval,
qs={'realurl': request.GET.get('realurl')},
chains=settings.PAYPAL_CHAINS))
slug=slug,
uuid=uuid_))
except:
log.error('Error getting paykey, purchase of addon: %s' % addon.pk,
exc_info=True)
@ -525,7 +536,22 @@ def purchase(request, addon):
uuid=str(uuid_), type=amo.CONTRIB_PENDING,
paykey=paykey, user=request.amo_user)
log.debug('Storing contrib for uuid: %s' % uuid_)
# If this was a pre-approval, it's completed already, we'll
# double check this with PayPal, just to be sure nothing went wrong.
if status == 'COMPLETED':
log.debug('Status is completed for uuid: %s' % uuid_)
if paypal.check_purchase(paykey) == 'COMPLETED':
log.debug('Check purchase is completed for uuid: %s' % uuid_)
contrib.type = amo.CONTRIB_PURCHASE
else:
# In this case PayPal disagreed, we should not be trusting
# what get_paykey said. Which is a worry.
log.error('Check purchase failed on uuid: %s' % uuid_)
status = 'NOT-COMPLETED'
contrib.save()
else:
log.error('No paykey present for uuid: %s' % uuid_)
@ -535,9 +561,16 @@ def purchase(request, addon):
if request.GET.get('result_type') == 'json' or request.is_ajax():
return http.HttpResponse(json.dumps({'url': url,
'paykey': paykey,
'error': error}),
'error': error,
'status': status}),
content_type='application/json')
return http.HttpResponseRedirect(url)
# This is the non-Ajax fallback.
if status != 'COMPLETED':
return redirect(url)
messages.success(request, _('Purchase complete'))
return redirect(shared_url('addons.detail', addon))
# TODO(andym): again, remove this once we figure out logged out flow.
@ -639,12 +672,17 @@ def contribute(request, addon):
# l10n: {0} is the addon name
contrib_for = _(u'Contribution for {0}').format(jinja2.escape(name))
paykey, error = '', ''
paykey, error, status = '', '', ''
try:
paykey = paypal.get_paykey(dict(uuid=contribution_uuid,
slug=addon.slug, amount=amount, email=paypal_id,
memo=contrib_for, ip=request.META.get('REMOTE_ADDR'),
pattern='%s.paypal' % ('apps' if webapp else 'addons')))
paykey, status = paypal.get_paykey(dict(
amount=amount,
email=paypal_id,
ip=request.META.get('REMOTE_ADDR'),
memo=contrib_for,
pattern='%s.paypal' %
('apps' if webapp else 'addons'),
slug=addon.slug,
uuid=contribution_uuid))
except:
log.error('Error getting paykey, contribution for addon: %s'
% addon.pk, exc_info=True)
@ -672,7 +710,8 @@ def contribute(request, addon):
# not have a paykey and the JS can cope appropriately.
return http.HttpResponse(json.dumps({'url': url,
'paykey': paykey,
'error': error}),
'error': error,
'status': status}),
content_type='application/json')
return http.HttpResponseRedirect(url)

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

@ -24,7 +24,18 @@ class AuthError(PaypalError):
pass
class PreApprovalError(PaypalError):
pass
errors = {'520003': AuthError}
# See http://bit.ly/vWV525 for information on these values.
# Note that if you have and invalid preapproval key you get 580022, but this
# also occurs in other cases so don't assume its preapproval only.
for number in ['579024', '579025', '579026', '579027', '579028',
'579030', '579031']:
errors[number] = PreApprovalError
paypal_log = commonware.log.getLogger('z.paypal')
@ -36,7 +47,7 @@ def should_ignore_paypal():
return settings.DEBUG and 'sandbox' not in settings.PAYPAL_PERMISSIONS_URL
def add_receivers(chains, email, amount, uuid):
def add_receivers(chains, email, amount, uuid, preapproval=False):
"""
Split a payment down into multiple receivers using the chains passed in.
"""
@ -47,23 +58,30 @@ def add_receivers(chains, email, amount, uuid):
this = (Decimal(str(float(amount) * (percent / 100.0)))
.quantize(Decimal('.01')))
remainder = remainder - this
key = 'receiverList.receiver(%s)' % number
result.update({
'receiverList.receiver(%s).email' % number: destination,
'receiverList.receiver(%s).amount' % number: str(this),
'receiverList.receiver(%s).paymentType' % number: 'DIGITALGOODS',
'receiverList.receiver(%s).primary' % number: 'false',
'%s.email' % key: destination,
'%s.amount' % key: str(this),
'%s.primary' % key: 'false',
# This is only done if there is a chained payment. Otherwise
# it does not need to be set.
'receiverList.receiver(0).primary': 'true',
# Mozilla pays the fees, because we've got a special rate.
'feesPayer': 'SECONDARYONLY'
})
if not preapproval:
result['%s.paymentType' % key] = 'DIGITALGOODS'
result.update({
'receiverList.receiver(0).email': email,
'receiverList.receiver(0).amount': str(amount),
'receiverList.receiver(0).invoiceID': 'mozilla-%s' % uuid,
'receiverList.receiver(0).paymentType': 'DIGITALGOODS',
'receiverList.receiver(0).invoiceID': 'mozilla-%s' % uuid
})
# Adding DIGITALGOODS to a pre-approval triggers an error in PayPal.
if not preapproval:
result['receiverList.receiver(0).paymentType'] = 'DIGITALGOODS'
return result
@ -94,17 +112,39 @@ def get_paykey(data):
'trackingId': data['uuid'],
'ipnNotificationUrl': absolutify(reverse('amo.paypal'))}
paypal_data.update(add_receivers(data.get('chains', ()), data['email'],
data['amount'], data['uuid']))
receivers = (data.get('chains', ()), data['email'], data['amount'],
data['uuid'])
if 'preapproval' in data:
# The paypal_key might be empty if they have removed it.
key = data['preapproval'].paypal_key
if key:
paypal_log.info('Using preapproval: %s' % data['preapproval'].pk)
paypal_data['preapprovalKey'] = key
paypal_data.update(add_receivers(*receivers, preapproval=True))
else:
paypal_data.update(add_receivers(*receivers))
if data.get('memo'):
paypal_data['memo'] = data['memo']
with statsd.timer('paypal.paykey.retrieval'):
response = _call(settings.PAYPAL_PAY_URL + 'Pay', paypal_data,
ip=data['ip'])
try:
with statsd.timer('paypal.paykey.retrieval'):
response = _call(settings.PAYPAL_PAY_URL + 'Pay', paypal_data,
ip=data['ip'])
except PreApprovalError, e:
# Let's retry just once without preapproval.
paypal_log.error('Failed using preapproval, reason: %s' % e)
# Now it's not a pre-approval, make sure we get the
# DIGITALGOODS setting back in there.
del paypal_data['preapprovalKey']
paypal_data.update(add_receivers(*receivers))
# If this fails, we won't try again, just fail.
with statsd.timer('paypal.paykey.retrieval'):
response = _call(settings.PAYPAL_PAY_URL + 'Pay', paypal_data,
ip=data['ip'])
return response['payKey']
return response['payKey'], response['paymentExecStatus']
def check_purchase(paykey):

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

@ -6,6 +6,7 @@ import urlparse
from django.conf import settings
import mock
from mock import Mock
from nose.tools import eq_
import time
@ -15,7 +16,6 @@ from amo.urlresolvers import reverse
import amo.tests
import paypal
good_response = ('responseEnvelope.timestamp='
'2011-01-28T06%3A16%3A33.259-08%3A00&responseEnvelope.ack=Success'
'&responseEnvelope.correlationId=7377e6ae1263c'
@ -26,8 +26,7 @@ auth_error = ('error(0).errorId=520003'
'&error(0).message=Authentication+failed.+API+'
'credentials+are+incorrect.')
other_error = ('error(0).errorId=520001'
'&error(0).message=Foo')
other_error = ('error(0).errorId=520001&error(0).message=Foo')
good_check_purchase = ('status=CREATED') # There is more, but I trimmed it.
@ -40,6 +39,13 @@ class TestPayKey(amo.tests.TestCase):
'uuid': time.time(),
'ip': '127.0.0.1',
'pattern': 'addons.purchase.finished'}
self.pre = Mock()
self.pre.paypal_key = 'xyz'
def get_pre_data(self):
data = self.data.copy()
data['preapproval'] = self.pre
return data
@mock.patch('urllib2.OpenerDirector.open')
def test_auth_fails(self, opener):
@ -49,7 +55,7 @@ class TestPayKey(amo.tests.TestCase):
@mock.patch('urllib2.OpenerDirector.open')
def test_get_key(self, opener):
opener.return_value = StringIO(good_response)
eq_(paypal.get_paykey(self.data), 'AP-9GD76073HJ780401K')
eq_(paypal.get_paykey(self.data), ('AP-9GD76073HJ780401K', 'CREATED'))
@mock.patch('urllib2.OpenerDirector.open')
def test_other_fails(self, opener):
@ -60,7 +66,7 @@ class TestPayKey(amo.tests.TestCase):
def test_qs_passed(self, _call):
data = self.data.copy()
data['qs'] = {'foo': 'bar'}
_call.return_value = {'payKey': '123'}
_call.return_value = {'payKey': '123', 'paymentExecStatus': ''}
paypal.get_paykey(data)
qs = _call.call_args[0][1]['returnUrl'].split('?')[1]
eq_(dict(urlparse.parse_qsl(qs))['foo'], 'bar')
@ -100,7 +106,7 @@ class TestPayKey(amo.tests.TestCase):
@mock.patch('paypal._call')
def test_dict_no_split(self, _call):
data = self.data.copy()
_call.return_value = {'payKey': '123'}
_call.return_value = {'payKey': '123', 'paymentExecStatus': ''}
paypal.get_paykey(data)
eq_(_call.call_args[0][1]['receiverList.receiver(0).amount'], '10')
@ -108,7 +114,7 @@ class TestPayKey(amo.tests.TestCase):
def test_dict_split(self, _call):
data = self.data.copy()
data['chains'] = ((13.4, 'us@moz.com'),)
_call.return_value = {'payKey': '123'}
_call.return_value = {'payKey': '123', 'paymentExecStatus': ''}
paypal.get_paykey(data)
eq_(_call.call_args[0][1]['receiverList.receiver(0).amount'], '10')
eq_(_call.call_args[0][1]['receiverList.receiver(1).amount'], '1.34')
@ -122,6 +128,55 @@ class TestPayKey(amo.tests.TestCase):
res = paypal.add_receivers(chains, 'a@a.com', Decimal('1.99'), '123')
eq_(res['feesPayer'], 'SECONDARYONLY')
@mock.patch('paypal._call')
def test_not_preapproval_key(self, _call):
_call.return_value = {'payKey': '123', 'paymentExecStatus': ''}
paypal.get_paykey(self.data)
assert 'preapprovalKey' not in _call.call_args[0][1]
@mock.patch('paypal._call')
def test_preapproval_key(self, _call):
_call.return_value = {'payKey': '123', 'paymentExecStatus': ''}
paypal.get_paykey(self.get_pre_data())
called = _call.call_args[0][1]
eq_(called['preapprovalKey'], 'xyz')
assert 'receiverList.receiver(0).paymentType' not in called
@mock.patch('paypal._call')
def test_preapproval_key_split(self, _call):
_call.return_value = {'payKey': '123', 'paymentExecStatus': ''}
data = self.get_pre_data()
data['chains'] = ((13.4, 'us@moz.com'),)
paypal.get_paykey(data)
called = _call.call_args[0][1]
assert 'receiverList.receiver(0).paymentType' not in called
assert 'receiverList.receiver(1).paymentType' not in called
@mock.patch('paypal._call')
def test_preapproval_retry(self, _call):
# Trigger an error on the preapproval and then pass.
def error_if(*args, **kw):
if 'preapprovalKey' in args[1]:
raise paypal.PreApprovalError('some error')
return {'payKey': '123', 'paymentExecStatus': ''}
_call.side_effect = error_if
res = paypal.get_paykey(self.get_pre_data())
eq_(_call.call_count, 2)
eq_(res[0], '123')
@mock.patch('paypal._call')
def test_preapproval_both_fail(self, _call):
# Trigger an error on the preapproval and then fail again.
def error_if(*args, **kw):
if 'preapprovalKey' in args[1]:
raise paypal.PreApprovalError('some error')
raise paypal.PaypalError('other error')
_call.side_effect = error_if
self.assertRaises(paypal.PaypalError, paypal.get_paykey,
self.get_pre_data())
class TestPurchase(amo.tests.TestCase):

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

@ -21,7 +21,9 @@ var purchases = {
success: function(json) {
$(el).removeClass(classes);
$('.modal').trigger('close'); // Hide all modals
if (json.paykey) {
if (json.status == 'COMPLETED') {
modalFromURL($(el).attr('data-thanksurl'));
} else if (json.paykey) {
/* This is supposed to be a global */
//dgFlow = new PAYPAL.apps.DGFlow({expType:'mini'});
dgFlow = new PAYPAL.apps.DGFlow({clicked: el.id});