268 строки
11 KiB
Python
268 строки
11 KiB
Python
import hashlib
|
|
import json
|
|
import uuid
|
|
|
|
from django import http
|
|
from django.conf import settings
|
|
from django.db.models import Q
|
|
from django.shortcuts import get_object_or_404, redirect
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
|
|
import commonware.log
|
|
import jingo
|
|
from tower import ugettext as _
|
|
import waffle
|
|
|
|
import amo
|
|
from amo import messages
|
|
from amo.decorators import login_required, post_required, write
|
|
from addons.decorators import (addon_view_factory, can_be_purchased,
|
|
has_not_purchased)
|
|
from lib.pay_server import client
|
|
from market.forms import PriceCurrencyForm
|
|
from market.models import AddonPurchase
|
|
import paypal
|
|
from stats.models import Contribution
|
|
from mkt.account.views import preapproval as user_preapproval
|
|
from mkt.webapps.models import Webapp
|
|
|
|
log = commonware.log.getLogger('z.purchase')
|
|
addon_view = addon_view_factory(qs=Webapp.objects.valid)
|
|
|
|
|
|
@login_required
|
|
@addon_view
|
|
@can_be_purchased
|
|
@has_not_purchased
|
|
@write
|
|
@post_required
|
|
def purchase(request, addon):
|
|
log.debug('Starting purchase of addon: %s by user: %s'
|
|
% (addon.pk, request.amo_user.pk))
|
|
amount = addon.premium.get_price()
|
|
source = request.POST.get('source', '')
|
|
uuid_ = hashlib.md5(str(uuid.uuid4())).hexdigest()
|
|
# L10n: {0} is the addon name.
|
|
contrib_for = (_(u'Mozilla Marketplace purchase of {0}')
|
|
.format(addon.name))
|
|
|
|
# Default is USD.
|
|
amount, currency = addon.premium.get_price(), 'USD'
|
|
|
|
# If tier is specified, then let's look it up.
|
|
if waffle.switch_is_active('currencies'):
|
|
form = PriceCurrencyForm(data=request.POST, addon=addon)
|
|
if form.is_valid():
|
|
tier = form.get_tier()
|
|
if tier:
|
|
amount, currency = tier.price, tier.currency
|
|
|
|
if not amount:
|
|
# We won't write a contribution row for this because there
|
|
# will not be a valid Paypal transaction. But we have to write the
|
|
# Purchase row, something that writing to the contribution normally
|
|
# does for us.
|
|
AddonPurchase.objects.safer_get_or_create(addon=addon,
|
|
user=request.amo_user)
|
|
return http.HttpResponse(json.dumps({'url': '', 'paykey': '',
|
|
'error': '',
|
|
'status': 'COMPLETED'}),
|
|
content_type='application/json')
|
|
|
|
paykey, status, error = '', '', ''
|
|
|
|
# TODO(solitude): remove this, pre-approval and currency will be
|
|
# stored in solitude.
|
|
preapproval = None
|
|
if (not waffle.flag_is_active(request, 'solitude-payments')
|
|
and request.amo_user):
|
|
preapproval = request.amo_user.get_preapproval()
|
|
# User the users default currency.
|
|
if currency == 'USD' and preapproval and preapproval.currency:
|
|
currency = preapproval.currency
|
|
|
|
if waffle.flag_is_active(request, 'solitude-payments'):
|
|
# TODO(solitude): when the migration of data is completed, we
|
|
# will be able to remove this. Seller data is populated in solitude
|
|
# on submission or devhub changes. If those don't occur, you won't be
|
|
# able to sell at all.
|
|
client.create_seller_for_pay(addon)
|
|
|
|
# Now call the client.
|
|
result = {}
|
|
try:
|
|
result = client.pay({'amount': amount, 'currency': currency,
|
|
'buyer': request.amo_user, 'seller': addon,
|
|
'memo': contrib_for})
|
|
except client.Error as error:
|
|
# Note that by assigning this to error, it will go into the return
|
|
# value for the json. General solitude errors will then be
|
|
# reported back to the user.
|
|
paypal.paypal_log_cef(request, addon, uuid_,
|
|
'PayKey Failure', 'PAYKEYFAIL',
|
|
'There was an error getting the paykey')
|
|
log.error('Error getting paykey: %s' % addon.pk, exc_info=True)
|
|
|
|
# TODO(solitude): just use the dictionary when solitude is live.
|
|
paykey = result.get('pay_key', '')
|
|
status = result.get('status', '')
|
|
uuid_ = result.get('uuid', '')
|
|
|
|
else:
|
|
# TODO(solitude): remove this when solitude goes live.
|
|
try:
|
|
paykey, status = paypal.get_paykey(dict(
|
|
amount=amount,
|
|
chains=settings.PAYPAL_CHAINS,
|
|
currency=currency,
|
|
email=addon.paypal_id,
|
|
ip=request.META.get('REMOTE_ADDR'),
|
|
memo=contrib_for,
|
|
pattern='purchase.done',
|
|
preapproval=preapproval,
|
|
qs={'realurl': request.POST.get('realurl')},
|
|
slug=addon.app_slug,
|
|
uuid=uuid_
|
|
))
|
|
except paypal.PaypalError as error:
|
|
paypal.paypal_log_cef(request, addon, uuid_,
|
|
'PayKey Failure', 'PAYKEYFAIL',
|
|
'There was an error getting the paykey')
|
|
log.error('Error getting paykey, purchase of addon: %s' % addon.pk,
|
|
exc_info=True)
|
|
|
|
if paykey:
|
|
# TODO(solitude): at some point we'll have to see what to do with
|
|
# contributions.
|
|
contrib = Contribution(addon_id=addon.id, amount=amount,
|
|
source=source, source_locale=request.LANG,
|
|
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':
|
|
paypal.paypal_log_cef(request, addon, uuid_,
|
|
'Purchase', 'PURCHASE',
|
|
'A user purchased using pre-approval')
|
|
|
|
log.debug('Status completed for uuid: %s' % uuid_)
|
|
if waffle.flag_is_active(request, 'solitude-payments'):
|
|
result = client.post_pay_check(data={'pay_key': paykey})
|
|
if result['status'] == 'COMPLETED':
|
|
contrib.type = amo.CONTRIB_PURCHASE
|
|
else:
|
|
log.error('Check purchase failed on uuid: %s' % uuid_)
|
|
status = 'NOT-COMPLETED'
|
|
|
|
else:
|
|
#TODO(solitude): remove this when solitude goes live.
|
|
if paypal.check_purchase(paykey) == 'COMPLETED':
|
|
log.debug('Check purchase 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_)
|
|
|
|
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.POST.get('result_type') == 'json' or request.is_ajax():
|
|
return http.HttpResponse(json.dumps({'url': url,
|
|
'paykey': paykey,
|
|
'error': str(error),
|
|
'status': status}),
|
|
content_type='application/json')
|
|
|
|
# This is the non-Ajax fallback.
|
|
if status != 'COMPLETED':
|
|
return redirect(url)
|
|
|
|
messages.success(request, _('Purchase complete'))
|
|
return redirect(addon.get_detail_url())
|
|
|
|
|
|
@csrf_exempt
|
|
@login_required
|
|
@addon_view
|
|
@can_be_purchased
|
|
@write
|
|
def purchase_done(request, addon, status):
|
|
result = ''
|
|
if status == 'complete':
|
|
uuid_ = request.GET.get('uuid')
|
|
log.debug('Looking up contrib for uuid: %s' % uuid_)
|
|
|
|
# The IPN may, or may not have come through. Which means looking for
|
|
# a for pre or post IPN contributions. If both fail, then we've not
|
|
# got a matching contribution.
|
|
#
|
|
# TODO(solitude): this will change when we figure out what to do
|
|
# with Contributions.
|
|
lookup = (Q(uuid=uuid_, type=amo.CONTRIB_PENDING) |
|
|
Q(transaction_id=uuid_, type=amo.CONTRIB_PURCHASE))
|
|
con = get_object_or_404(Contribution, lookup)
|
|
|
|
log.debug('Check purchase addon: %s, user: %s, paykey: %s'
|
|
% (addon.pk, request.amo_user.pk, con.paykey[:10]))
|
|
|
|
if waffle.flag_is_active(request, 'solitude-payments'):
|
|
try:
|
|
res = client.post_pay_check(data={'uuid': uuid_})
|
|
except client.Error:
|
|
paypal.paypal_log_cef(request, addon, uuid_,
|
|
'Purchase Fail', 'PURCHASEFAIL',
|
|
'Checking purchase error')
|
|
raise
|
|
|
|
result = res['status']
|
|
|
|
# TODO(solitude): can be removed once solitude is live.
|
|
else:
|
|
try:
|
|
result = paypal.check_purchase(con.paykey)
|
|
if result == 'ERROR':
|
|
paypal.paypal_log_cef(request, addon, uuid_,
|
|
'Purchase Fail', 'PURCHASEFAIL',
|
|
'Checking purchase error')
|
|
raise
|
|
except:
|
|
paypal.paypal_log_cef(request, addon, uuid_,
|
|
'Purchase Fail', 'PURCHASEFAIL',
|
|
'There was an error checking purchase')
|
|
log.error('Check purchase addon: %s, user: %s, paykey: %s'
|
|
% (addon.pk, request.amo_user.pk, con.paykey[:10]),
|
|
exc_info=True)
|
|
result = 'ERROR'
|
|
status = 'error'
|
|
|
|
log.debug('Paypal returned: %s for paykey: %s'
|
|
% (result, con.paykey[:10]))
|
|
if result == 'COMPLETED' and con.type == amo.CONTRIB_PENDING:
|
|
amo.log(amo.LOG.PURCHASE_ADDON, addon)
|
|
con.update(type=amo.CONTRIB_PURCHASE)
|
|
|
|
context = {'realurl': request.GET.get('realurl', ''),
|
|
'status': status, 'result': result, 'product': addon,
|
|
'error': amo.PAYMENT_DETAILS_ERROR.get(result, '')}
|
|
|
|
response = jingo.render(request, 'purchase/done.html', context)
|
|
response['x-frame-options'] = 'allow'
|
|
return response
|
|
|
|
|
|
@post_required
|
|
@login_required
|
|
@addon_view
|
|
def preapproval(request, addon):
|
|
return user_preapproval(request, complete=addon.get_detail_url(),
|
|
cancel=addon.get_detail_url())
|