addons-server/mkt/developers/views_payments.py

382 строки
14 KiB
Python

import json
import urllib
from datetime import datetime
from django import http
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404, redirect
import commonware
from curling.lib import HttpClientError
import jingo
from jingo import helpers
import jinja2
import waffle
from tower import ugettext as _
from waffle.decorators import waffle_switch
import amo
from access import acl
from amo import messages
from amo.decorators import json_view, login_required, post_required, write
from amo.urlresolvers import reverse
from constants.payments import (PAYMENT_METHOD_ALL,
PAYMENT_METHOD_CARD,
PAYMENT_METHOD_OPERATOR)
from lib.crypto import generate_key
from lib.pay_server import client
from market.models import Price
from mkt.constants import DEVICE_LOOKUP
from mkt.developers.decorators import dev_required
from mkt.developers.models import PaymentAccount, UserInappKey, uri_to_pk
from . import forms, forms_payments
log = commonware.log.getLogger('z.devhub')
@dev_required
@post_required
def disable_payments(request, addon_id, addon):
addon.update(wants_contributions=False)
return redirect(addon.get_dev_url('payments'))
@dev_required(owner_for_post=True, webapp=True)
def payments(request, addon_id, addon, webapp=False):
premium_form = forms_payments.PremiumForm(
request.POST or None, request=request, addon=addon,
user=request.amo_user)
region_form = forms.RegionForm(
request.POST or None, product=addon, request=request)
upsell_form = forms_payments.UpsellForm(
request.POST or None, addon=addon, user=request.amo_user)
bango_account_list_form = forms_payments.BangoAccountListForm(
request.POST or None, addon=addon, user=request.amo_user)
if request.method == 'POST':
success = all(form.is_valid() for form in
[premium_form, region_form, upsell_form,
bango_account_list_form])
if success:
region_form.save()
try:
premium_form.save()
except client.Error as err:
success = False
log.error('Error setting payment information (%s)' % err)
messages.error(
request, _(u'We encountered a problem connecting to the '
u'payment server.'))
raise # We want to see these exceptions!
is_free_inapp = addon.premium_type == amo.ADDON_FREE_INAPP
is_now_paid = (addon.premium_type in amo.ADDON_PREMIUMS
or is_free_inapp)
# If we haven't changed to a free app, check the upsell.
if is_now_paid and success:
try:
if not is_free_inapp:
upsell_form.save()
bango_account_list_form.save()
except client.Error as err:
log.error('Error saving payment information (%s)' % err)
messages.error(
request, _(u'We encountered a problem connecting to '
u'the payment server.'))
success = False
raise # We want to see all the solitude errors now.
# If everything happened successfully, give the user a pat on the back.
if success:
messages.success(request, _('Changes successfully saved.'))
return redirect(addon.get_dev_url('payments'))
# TODO: This needs to be updated as more platforms support payments.
cannot_be_paid = (
addon.premium_type == amo.ADDON_FREE and
any(premium_form.device_data['free-%s' % x] == y for x, y in
[('android-mobile', True), ('android-tablet', True),
('desktop', True), ('firefoxos', False)]))
try:
tier_zero = Price.objects.get(price='0.00', active=True)
tier_zero_id = tier_zero.pk
except Price.DoesNotExist:
tier_zero = None
tier_zero_id = ''
# Get the regions based on tier zero. This should be all the
# regions with payments enabled.
paid_region_ids_by_slug = []
if tier_zero:
paid_region_ids_by_slug = tier_zero.region_ids_by_slug()
return jingo.render(
request, 'developers/payments/premium.html',
{'addon': addon, 'webapp': webapp, 'premium': addon.premium,
'form': premium_form, 'upsell_form': upsell_form,
'tier_zero_id': tier_zero_id,
'region_form': region_form,
'DEVICE_LOOKUP': DEVICE_LOOKUP,
'is_paid': (addon.premium_type in amo.ADDON_PREMIUMS
or addon.premium_type == amo.ADDON_FREE_INAPP),
'no_paid': cannot_be_paid,
'is_incomplete': addon.status == amo.STATUS_NULL,
'is_packaged': addon.is_packaged,
# Bango values
'bango_account_form': forms_payments.BangoPaymentAccountForm(),
'bango_account_list_form': bango_account_list_form,
# Waffles
'payments_enabled':
waffle.flag_is_active(request, 'allow-b2g-paid-submission') and
not waffle.switch_is_active('disabled-payments'),
'api_pricelist_url':
reverse('api_dispatch_list', kwargs={'resource_name': 'prices',
'api_name': 'webpay'}),
'payment_methods': {
PAYMENT_METHOD_ALL: _('All'),
PAYMENT_METHOD_CARD: _('Credit card'),
PAYMENT_METHOD_OPERATOR: _('Carrier'),
},
'all_paid_region_ids_by_slug': paid_region_ids_by_slug,
})
@login_required
@json_view
def payment_accounts(request):
app_slug = request.GET.get('app-slug', '')
accounts = PaymentAccount.objects.filter(
user=request.amo_user, inactive=False)
def account(acc):
app_names = (', '.join(unicode(apa.addon.name)
for apa in acc.addonpaymentaccount_set.all()))
data = {
'id': acc.pk,
'name': jinja2.escape(unicode(acc)),
'app-names': jinja2.escape(app_names),
'account-url':
reverse('mkt.developers.bango.payment_account', args=[acc.pk]),
'delete-url':
reverse('mkt.developers.bango.delete_payment_account',
args=[acc.pk]),
'agreement-url': acc.get_agreement_url(),
'agreement': 'accepted' if acc.agreed_tos else 'rejected'
}
if waffle.switch_is_active('bango-portal') and app_slug:
data['portal-url'] = reverse(
'mkt.developers.apps.payments.bango_portal', args=[app_slug])
return data
return map(account, accounts)
@login_required
def payment_accounts_form(request):
bango_account_form = forms_payments.BangoAccountListForm(
user=request.amo_user, addon=None)
return jingo.render(
request, 'developers/payments/includes/bango_accounts_form.html',
{'bango_account_list_form': bango_account_form})
@write
@post_required
@login_required
@json_view
def payments_accounts_add(request):
form = forms_payments.BangoPaymentAccountForm(request.POST)
if not form.is_valid():
return json_view.error(form.errors)
try:
obj = PaymentAccount.create_bango(request.amo_user, form.cleaned_data)
except HttpClientError as e:
log.error('Client error create Bango account; %s' % e)
return http.HttpResponseBadRequest(json.dumps(e.content))
return {'pk': obj.pk, 'agreement-url': obj.get_agreement_url()}
@write
@login_required
@json_view
def payments_account(request, id):
account = get_object_or_404(PaymentAccount, pk=id, user=request.user)
if request.POST:
form = forms_payments.BangoPaymentAccountForm(
request.POST, account=account)
if form.is_valid():
form.save()
else:
return json_view.error(form.errors)
return account.get_details()
@write
@post_required
@login_required
def payments_accounts_delete(request, id):
account = get_object_or_404(PaymentAccount, pk=id, user=request.user)
account.cancel(disable_refs=True)
log.info('Account cancelled: %s' % id)
return http.HttpResponse('success')
@login_required
@waffle_switch('in-app-sandbox')
def in_app_keys(request):
keys = (UserInappKey.objects.no_cache()
.filter(solitude_seller__user=request.amo_user))
# TODO(Kumar) support multiple test keys. For now there's only one.
if keys.count():
key = keys.get()
else:
key = None
if request.method == 'POST':
if key:
key.reset()
messages.success(request, _('Secret was reset successfully.'))
else:
UserInappKey.create(request.amo_user)
messages.success(request,
_('Key and secret were created successfully.'))
return redirect(reverse('mkt.developers.apps.in_app_keys'))
return jingo.render(request, 'developers/payments/in-app-keys.html',
{'key': key})
@login_required
@waffle_switch('in-app-sandbox')
def in_app_key_secret(request, pk):
key = (UserInappKey.objects.no_cache()
.filter(solitude_seller__user=request.amo_user, pk=pk))
if not key.count():
# Either the record does not exist or it's not owned by the
# logged in user.
return http.HttpResponseForbidden()
return http.HttpResponse(key.get().secret())
@login_required
@waffle_switch('in-app-payments')
@dev_required(owner_for_post=True, webapp=True)
def in_app_config(request, addon_id, addon, webapp=True):
inapp = addon.premium_type in amo.ADDON_INAPPS
if not inapp:
messages.error(request,
_('Your app is not configured for in-app payments.'))
return redirect(reverse('mkt.developers.apps.payments',
args=[addon.app_slug]))
try:
account = addon.app_payment_account
except ObjectDoesNotExist:
messages.error(request, _('No payment account for this app.'))
return redirect(reverse('mkt.developers.apps.payments',
args=[addon.app_slug]))
seller_config = get_seller_product(account)
owner = acl.check_addon_ownership(request, addon)
if request.method == 'POST':
# Reset the in-app secret for the app.
(client.api.generic
.product(seller_config['resource_pk'])
.patch(data={'secret': generate_key(48)}))
messages.success(request, _('Changes successfully saved.'))
return redirect(reverse('mkt.developers.apps.in_app_config',
args=[addon.app_slug]))
return jingo.render(request, 'developers/payments/in-app-config.html',
{'addon': addon, 'owner': owner,
'seller_config': seller_config})
@login_required
@waffle_switch('in-app-payments')
@dev_required(webapp=True)
def in_app_secret(request, addon_id, addon, webapp=True):
seller_config = get_seller_product(addon.app_payment_account)
return http.HttpResponse(seller_config['secret'])
@waffle_switch('bango-portal')
@dev_required(webapp=True)
def bango_portal(request, addon_id, addon, webapp=True):
if not ((addon.authors.filter(user=request.user,
addonuser__role=amo.AUTHOR_ROLE_OWNER).exists()) and
(addon.app_payment_account.payment_account.solitude_seller.user.id
== request.user.id)):
log.error(('User not allowed to reach the Bango portal; '
'pk=%s') % request.user.pk)
return http.HttpResponseForbidden()
package_id = addon.app_payment_account.payment_account.bango_package_id
try:
bango_token = client.api.bango.login.post({'packageId': package_id})
except HttpClientError as e:
log.error('Failed to authenticate against Bango portal; %s' % addon_id,
exc_info=True)
return http.HttpResponseBadRequest(json.dumps(e.content))
bango_url = '{base_url}{parameters}'.format(**{
'base_url': settings.BANGO_BASE_PORTAL_URL,
'parameters': urllib.urlencode({
'authenticationToken': bango_token['authentication_token'],
'emailAddress': bango_token['email_address'],
'packageId': package_id,
'personId': bango_token['person_id'],
})
})
response = http.HttpResponse(status=204)
response['Location'] = bango_url
return response
def get_seller_product(account):
"""
Get the solitude seller_product for a payment account object.
"""
bango_product = (client.api.bango
.product(uri_to_pk(account.product_uri))
.get_object_or_404())
# TODO(Kumar): we can optimize this by storing the seller_product
# when we create it in developers/models.py or allowing solitude
# to filter on both fields.
return (client.api.generic
.product(uri_to_pk(bango_product['seller_product']))
.get_object_or_404())
# TODO(andym): move these into a tastypie API.
@login_required
@json_view
def agreement(request, id):
account = get_object_or_404(PaymentAccount, pk=id, user=request.user)
# It's a shame we have to do another get to find this out.
package = client.api.bango.package(account.uri).get_object_or_404()
if request.method == 'POST':
# Set the agreement.
account.update(agreed_tos=True)
return (client.api.bango.sbi.post(
data={'seller_bango': package['resource_uri']}))
res = (client.api.bango.sbi.agreement
.get_object(data={'seller_bango': package['resource_uri']}))
res['valid'] = helpers.datetime(
datetime.strptime(res['valid'], '%Y-%m-%dT%H:%M:%S'))
return res