ping paypal to find status after payment and record purchase (bug 684104)

This commit is contained in:
Andy McKay 2011-09-09 14:28:05 -07:00
Родитель b0af79630f
Коммит d85d5775da
7 изменённых файлов: 181 добавлений и 27 удалений

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

@ -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,14 +59,30 @@ 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)
raise
paypal_log.error('Authentication error: %s' % error)
raise
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