ping paypal to find status after payment and record purchase (bug 684104)
This commit is contained in:
Родитель
b0af79630f
Коммит
d85d5775da
|
@ -28,7 +28,7 @@ from addons import cron
|
|||
from addons.models import (Addon, AddonDependency, AddonUpsell, AddonUser,
|
||||
Charity, Category)
|
||||
from files.models import File
|
||||
from market.models import AddonPremium
|
||||
from market.models import AddonPremium, AddonPurchase
|
||||
from paypal.tests import other_error
|
||||
from stats.models import Contribution
|
||||
from translations.helpers import truncate
|
||||
|
@ -216,7 +216,7 @@ class TestContributeEmbedded(amo.tests.TestCase):
|
|||
|
||||
|
||||
class TestPurchaseEmbedded(amo.tests.TestCase):
|
||||
fixtures = ['base/apps', 'base/addon_592', 'prices']
|
||||
fixtures = ['base/apps', 'base/addon_592', 'base/users', 'prices']
|
||||
|
||||
def setUp(self):
|
||||
waffle.models.Switch.objects.create(name='marketplace', active=True)
|
||||
|
@ -225,6 +225,7 @@ class TestPurchaseEmbedded(amo.tests.TestCase):
|
|||
status=amo.STATUS_PUBLIC)
|
||||
AddonPremium.objects.create(addon=self.addon, price_id=1)
|
||||
self.purchase_url = reverse('addons.purchase', args=[self.addon.slug])
|
||||
self.client.login(username='regular@mozilla.com', password='password')
|
||||
|
||||
def test_premium_only(self):
|
||||
self.addon.update(premium_type=amo.ADDON_FREE)
|
||||
|
@ -256,6 +257,70 @@ class TestPurchaseEmbedded(amo.tests.TestCase):
|
|||
res = self.client.get_ajax(self.purchase_url)
|
||||
assert json.loads(res.content)['error'].startswith('There was an')
|
||||
|
||||
@patch('paypal.get_paykey')
|
||||
def test_paykey_contribution(self, get_paykey):
|
||||
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 make_contribution(self):
|
||||
return Contribution.objects.create(type=amo.CONTRIB_PENDING,
|
||||
uuid='123', addon=self.addon,
|
||||
paykey='1234')
|
||||
|
||||
def get_url(self, status):
|
||||
return reverse('addons.purchase.finished',
|
||||
args=[self.addon.slug, status])
|
||||
|
||||
@patch('paypal.check_purchase')
|
||||
def test_check_purchase(self, check_purchase):
|
||||
check_purchase.return_value = 'COMPLETED'
|
||||
self.make_contribution()
|
||||
self.client.get_ajax('%s?uuid=%s' % (self.get_url('complete'), '123'))
|
||||
cons = Contribution.objects.all()
|
||||
eq_(cons.count(), 1)
|
||||
eq_(cons[0].type, amo.CONTRIB_PURCHASE)
|
||||
eq_(cons[0].uuid, None)
|
||||
|
||||
@patch('paypal.check_purchase')
|
||||
def test_check_addon_purchase(self, check_purchase):
|
||||
check_purchase.return_value = 'COMPLETED'
|
||||
self.make_contribution()
|
||||
self.client.get_ajax('%s?uuid=%s' % (self.get_url('complete'), '123'))
|
||||
eq_(AddonPurchase.objects.filter(addon=self.addon).count(), 1)
|
||||
|
||||
@patch('paypal.check_purchase')
|
||||
def test_check_cancel(self, check_purchase):
|
||||
check_purchase.return_value = 'COMPLETED'
|
||||
self.make_contribution()
|
||||
self.client.get_ajax('%s?uuid=%s' % (self.get_url('cancel'), '123'))
|
||||
eq_(Contribution.objects.filter(type=amo.CONTRIB_PURCHASE).count(), 0)
|
||||
|
||||
@patch('paypal.check_purchase')
|
||||
def test_check_wrong_uuid(self, check_purchase):
|
||||
check_purchase.return_value = 'COMPLETED'
|
||||
self.make_contribution()
|
||||
self.assertRaises(Contribution.DoesNotExist,
|
||||
self.client.get_ajax,
|
||||
'%s?uuid=%s' % (self.get_url('complete'), 'foo'))
|
||||
|
||||
@patch('paypal.check_purchase')
|
||||
def test_check_pending(self, check_purchase):
|
||||
check_purchase.return_value = 'PENDING'
|
||||
self.make_contribution()
|
||||
self.client.get_ajax('%s?uuid=%s' % (self.get_url('complete'), '123'))
|
||||
eq_(Contribution.objects.filter(type=amo.CONTRIB_PURCHASE).count(), 0)
|
||||
|
||||
@patch('paypal.check_purchase')
|
||||
def test_check_pending_error(self, check_purchase):
|
||||
check_purchase.side_effect = Exception('wtf')
|
||||
self.make_contribution()
|
||||
url = '%s?uuid=%s' % (self.get_url('complete'), '123')
|
||||
res = self.client.get_ajax(url)
|
||||
eq_(res.context['status'], 'ERROR')
|
||||
|
||||
|
||||
class TestDeveloperPages(amo.tests.TestCase):
|
||||
fixtures = ['base/apps', 'base/addon_3615', 'base/addon_592',
|
||||
|
@ -351,7 +416,6 @@ class TestDeveloperPages(amo.tests.TestCase):
|
|||
addon.save()
|
||||
url = reverse('addons.meet', args=['592'])
|
||||
r = self.client.get(url, follow=True)
|
||||
doc = pq(r.content)
|
||||
eq_(pq(r.content)('#about-addon b').length, 2)
|
||||
|
||||
|
||||
|
|
|
@ -31,6 +31,8 @@ detail_patterns = patterns('',
|
|||
name='addons.paypal'),
|
||||
|
||||
url('^purchase/$', views.purchase, name='addons.purchase'),
|
||||
url('^purchase/(?P<status>cancel|complete)$', views.purchase_complete,
|
||||
name='addons.purchase.finished'),
|
||||
|
||||
url('^about$', lambda r, addon_id: redirect('addons.installed',
|
||||
addon_id, permanent=True),
|
||||
|
|
|
@ -33,6 +33,7 @@ from amo.urlresolvers import reverse
|
|||
from abuse.models import send_abuse_report
|
||||
from bandwagon.models import Collection, CollectionFeature, CollectionPromo
|
||||
from devhub.decorators import dev_required
|
||||
from market.models import AddonPurchase
|
||||
import paypal
|
||||
from reviews.forms import ReviewForm
|
||||
from reviews.models import Review, GroupedRating
|
||||
|
@ -465,27 +466,37 @@ def developers(request, addon, page):
|
|||
@login_required
|
||||
@addon_view
|
||||
def purchase(request, addon):
|
||||
if not waffle.switch_is_active('marketplace'):
|
||||
raise http.Http404
|
||||
|
||||
if (not addon.is_premium() or not addon.premium):
|
||||
if not waffle.switch_is_active('marketplace') or not addon.is_premium():
|
||||
raise http.Http404
|
||||
|
||||
log.debug('Starting purchase of addon: %s by user: %s'
|
||||
% (addon.pk, request.amo_user.pk))
|
||||
amount = addon.premium.price.price
|
||||
source = request.GET.get('source', '')
|
||||
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, error = '', ''
|
||||
try:
|
||||
paykey = paypal.get_paykey(dict(uuid=uuid_, slug=addon.slug,
|
||||
amount=amount, memo=contrib_for,
|
||||
email=addon.paypal_id,
|
||||
ip=request.META.get('REMOTE_ADDR')))
|
||||
except Exception:
|
||||
amount=amount, memo=contrib_for, email=addon.paypal_id,
|
||||
ip=request.META.get('REMOTE_ADDR'),
|
||||
pattern='addons.purchase.finished'))
|
||||
except:
|
||||
log.error('Error getting paykey, purchase of addon: %s' % addon.pk,
|
||||
exc_info=True)
|
||||
error = _('There was an error communicating with PayPal.')
|
||||
|
||||
if paykey:
|
||||
contrib = Contribution(addon_id=addon.id, amount=amount,
|
||||
source=source, source_locale=request.LANG,
|
||||
uuid=str(uuid_), type=amo.CONTRIB_PENDING,
|
||||
paykey=paykey)
|
||||
contrib.save()
|
||||
|
||||
log.debug('Got paykey for addon: %s by user: %s'
|
||||
% (addon.pk, request.amo_user.pk))
|
||||
url = '%s?paykey=%s' % (settings.PAYPAL_FLOW_URL, paykey)
|
||||
if request.GET.get('result_type') == 'json' or request.is_ajax():
|
||||
return http.HttpResponse(json.dumps({'url': url,
|
||||
|
@ -495,6 +506,44 @@ def purchase(request, addon):
|
|||
return http.HttpResponseRedirect(url)
|
||||
|
||||
|
||||
# TODO(andym): again, remove this onece we figure out logged out flow.
|
||||
@login_required
|
||||
@addon_view
|
||||
def purchase_complete(request, addon, status):
|
||||
if not waffle.switch_is_active('marketplace') or not addon.is_premium():
|
||||
raise http.Http404
|
||||
|
||||
result = ''
|
||||
if status == 'complete':
|
||||
con = Contribution.objects.get(uuid=request.GET.get('uuid'),
|
||||
type=amo.CONTRIB_PENDING)
|
||||
log.debug('Check purchase paypal addon: %s, user: %s, paykey: %s'
|
||||
% (addon.pk, request.amo_user.pk, con.paykey[:10]))
|
||||
try:
|
||||
result = paypal.check_purchase(con.paykey)
|
||||
except:
|
||||
log.error('Check purchase paypal addon: %s, user: %s, paykey: %s'
|
||||
% (addon.pk, request.amo_user.pk, con.paykey[:10]),
|
||||
exc_info=True)
|
||||
result = 'ERROR'
|
||||
|
||||
log.debug('Paypal returned: %s for paykey: %s'
|
||||
% (result, con.paykey[:10]))
|
||||
if result == 'COMPLETED':
|
||||
# Sadly we are changing things on a GET.
|
||||
# Create an addon purchase.
|
||||
AddonPurchase.objects.create(addon=addon, user=request.amo_user)
|
||||
# Markup the contribution suitably
|
||||
con.type = amo.CONTRIB_PURCHASE
|
||||
con.uuid = None
|
||||
con.save()
|
||||
|
||||
response = jingo.render(request, 'addons/paypal_result.html',
|
||||
{'addon': addon, 'status': result})
|
||||
response['x-frame-options'] = 'allow'
|
||||
return response
|
||||
|
||||
|
||||
@addon_view
|
||||
def contribute(request, addon):
|
||||
contrib_type = request.GET.get('type', 'suggested')
|
||||
|
@ -514,15 +563,16 @@ def contribute(request, addon):
|
|||
name, paypal_id = addon.charity.name, addon.charity.paypal
|
||||
else:
|
||||
name, paypal_id = addon.name, addon.paypal_id
|
||||
# l10n: {0} is the addon name
|
||||
contrib_for = _(u'Contribution for {0}').format(jinja2.escape(name))
|
||||
|
||||
paykey, error = '', ''
|
||||
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')))
|
||||
except Exception:
|
||||
slug=addon.slug, amount=amount, email=paypal_id,
|
||||
memo=contrib_for, ip=request.META.get('REMOTE_ADDR'),
|
||||
pattern='addons.paypal'))
|
||||
except:
|
||||
log.error('Error getting paykey, contribution for addon: %s'
|
||||
% addon.pk, exc_info=True)
|
||||
error = _('There was an error communicating with PayPal.')
|
||||
|
|
|
@ -281,6 +281,9 @@ CONTRIB_VOLUNTARY = 0
|
|||
CONTRIB_PURCHASE = 1
|
||||
CONTRIB_REFUND = 2
|
||||
CONTRIB_CHARGEBACK = 3
|
||||
# We've started a transaction and we need to wait to see what
|
||||
# paypal will return.
|
||||
CONTRIB_PENDING = 4
|
||||
CONTRIB_OTHER = 99
|
||||
|
||||
CONTRIB_TYPES = {
|
||||
|
|
|
@ -29,15 +29,15 @@ paypal_log = commonware.log.getLogger('z.paypal')
|
|||
def get_paykey(data):
|
||||
"""
|
||||
Gets a paykey from Paypal. Need to pass in the following in data:
|
||||
slug: addon, will form urls for where user goes back to (required)
|
||||
pattern: the reverse pattern to resolve
|
||||
email: who the money is going to (required)
|
||||
amount: the amount of money (required)
|
||||
ip: ip address of end user (required)
|
||||
uuid: contribution_uuid (required)
|
||||
memo: any nice message
|
||||
"""
|
||||
complete = reverse('addons.paypal', args=[data['slug'], 'complete'])
|
||||
cancel = reverse('addons.paypal', args=[data['slug'], 'cancel'])
|
||||
complete = reverse(data['pattern'], args=[data['slug'], 'complete'])
|
||||
cancel = reverse(data['pattern'], args=[data['slug'], 'cancel'])
|
||||
uuid_qs = urllib.urlencode({'uuid': data['uuid']})
|
||||
|
||||
paypal_data = {
|
||||
|
@ -59,7 +59,7 @@ def get_paykey(data):
|
|||
|
||||
with statsd.timer('paypal.paykey.retrieval'):
|
||||
try:
|
||||
response = _call(settings.PAYPAL_PAY_URL, paypal_data,
|
||||
response = _call(settings.PAYPAL_PAY_URL + 'Pay', paypal_data,
|
||||
ip=data['ip'])
|
||||
except AuthError, error:
|
||||
paypal_log.error('Authentication error: %s' % error)
|
||||
|
@ -67,6 +67,22 @@ def get_paykey(data):
|
|||
return response['payKey']
|
||||
|
||||
|
||||
def check_purchase(paykey):
|
||||
"""
|
||||
When a purchase is complete checks paypal that the purchase has gone
|
||||
through.
|
||||
"""
|
||||
with statsd.timer('paypal.payment.details'):
|
||||
try:
|
||||
response = _call(settings.PAYPAL_PAY_URL + 'PaymentDetails',
|
||||
{'payKey': paykey})
|
||||
except PaypalError:
|
||||
paypal_log.error('Payment details error', exc_info=True)
|
||||
return False
|
||||
|
||||
return response['status']
|
||||
|
||||
|
||||
def check_refund_permission(token):
|
||||
"""
|
||||
Asks PayPal whether the PayPal ID for this account has granted
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
from cStringIO import StringIO
|
||||
|
||||
from django.conf import settings
|
||||
from amo.urlresolvers import reverse
|
||||
from amo.helpers import absolutify
|
||||
|
||||
import mock
|
||||
from nose.tools import eq_
|
||||
|
@ -26,14 +24,17 @@ auth_error = ('error(0).errorId=520003'
|
|||
other_error = ('error(0).errorId=520001'
|
||||
'&error(0).message=Foo')
|
||||
|
||||
good_check_purchase = ('status=CREATED') # There is more, but I trimmed it.
|
||||
|
||||
class TestPayPal(amo.tests.TestCase):
|
||||
|
||||
class TestPayKey(amo.tests.TestCase):
|
||||
def setUp(self):
|
||||
self.data = {'slug': 'xx',
|
||||
'amount': 10,
|
||||
'email': 'someone@somewhere.com',
|
||||
'uuid': time.time(),
|
||||
'ip': '127.0.0.1'}
|
||||
'ip': '127.0.0.1',
|
||||
'pattern': 'addons.purchase.finished'}
|
||||
|
||||
@mock.patch('urllib2.OpenerDirector.open')
|
||||
def test_auth_fails(self, opener):
|
||||
|
@ -54,6 +55,24 @@ class TestPayPal(amo.tests.TestCase):
|
|||
# Remove _ and run if you'd like to try unmocked.
|
||||
return paypal.get_paykey(self.data)
|
||||
|
||||
def _test_check_purchase_no_mock(self):
|
||||
# Remove _ and run if you'd like to try this unmocked.
|
||||
key = paypal.get_paykey(self.data)
|
||||
eq_(paypal.check_purchase(key), 'CREATED')
|
||||
|
||||
|
||||
class TestPurchase(amo.tests.TestCase):
|
||||
|
||||
@mock.patch('urllib2.OpenerDirector.open')
|
||||
def test_check_purchase(self, opener):
|
||||
opener.return_value = StringIO(good_check_purchase)
|
||||
eq_(paypal.check_purchase('some-paykey'), 'CREATED')
|
||||
|
||||
@mock.patch('urllib2.OpenerDirector.open')
|
||||
def test_check_purchase_fails(self, opener):
|
||||
opener.return_value = StringIO(other_error)
|
||||
eq_(paypal.check_purchase('some-paykey'), False)
|
||||
|
||||
|
||||
@mock.patch('paypal.urllib.urlopen')
|
||||
def test_check_paypal_id(urlopen_mock):
|
||||
|
|
|
@ -814,7 +814,7 @@ PAYPAL_BN = ''
|
|||
PAYPAL_CGI_URL = 'https://www.paypal.com/cgi-bin/webscr'
|
||||
PAYPAL_CGI_AUTH = {'USER': '', 'PASSWORD': '', 'SIGNATURE': ''}
|
||||
|
||||
PAYPAL_PAY_URL = 'https://svcs.paypal.com/AdaptivePayments/Pay'
|
||||
PAYPAL_PAY_URL = 'https://svcs.paypal.com/AdaptivePayments/'
|
||||
PAYPAL_FLOW_URL = 'https://paypal.com/webapps/adaptivepayment/flow/pay'
|
||||
PAYPAL_PERMISSIONS_URL = 'https://svcs.paypal.com/Permissions/'
|
||||
PAYPAL_JS_URL = 'https://www.paypalobjects.com/js/external/dg.js'
|
||||
|
@ -1093,7 +1093,7 @@ ARECIBO_SERVER_URL = ""
|
|||
# Make AMO group posts and wait for 60 seconds when we get lots of errors.
|
||||
ARECIBO_SETTINGS = {
|
||||
'GROUP_POSTS': True,
|
||||
'GROUP_WAIT': 60
|
||||
'GROUP_WAIT': 60,
|
||||
}
|
||||
|
||||
# A whitelist of domains that the authentication script will redirect to upon
|
||||
|
|
Загрузка…
Ссылка в новой задаче