301 строка
11 KiB
Python
301 строка
11 KiB
Python
import uuid
|
|
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.db import models
|
|
|
|
import commonware.log
|
|
from tower import ugettext_lazy as _lazy
|
|
|
|
import amo
|
|
from amo.urlresolvers import reverse
|
|
from lib.crypto import generate_key
|
|
from lib.pay_server import client
|
|
from mkt.constants.payments import ACCESS_PURCHASE, ACCESS_SIMULATE
|
|
from mkt.purchase import webpay
|
|
from users.models import UserForeignKey
|
|
|
|
log = commonware.log.getLogger('z.devhub')
|
|
|
|
|
|
def uri_to_pk(uri):
|
|
"""
|
|
Convert a resource URI to the primary key of the resource.
|
|
"""
|
|
return uri.rstrip('/').split('/')[-1]
|
|
|
|
|
|
class SolitudeSeller(amo.models.ModelBase):
|
|
# TODO: When Solitude allows for it, this should be updated to be 1:1 with
|
|
# users.
|
|
user = UserForeignKey()
|
|
uuid = models.CharField(max_length=255, unique=True)
|
|
resource_uri = models.CharField(max_length=255)
|
|
|
|
class Meta:
|
|
db_table = 'payments_seller'
|
|
|
|
@classmethod
|
|
def create(cls, user):
|
|
uuid_ = str(uuid.uuid4())
|
|
res = client.api.generic.seller.post(data={'uuid': uuid_})
|
|
uri = res['resource_uri']
|
|
obj = cls.objects.create(user=user, uuid=uuid_, resource_uri=uri)
|
|
|
|
log.info('[User:%s] Created Solitude seller (uuid:%s)' %
|
|
(user, uuid_))
|
|
return obj
|
|
|
|
|
|
class PaymentAccount(amo.models.ModelBase):
|
|
user = UserForeignKey()
|
|
name = models.CharField(max_length=64)
|
|
agreed_tos = models.BooleanField()
|
|
solitude_seller = models.ForeignKey(SolitudeSeller)
|
|
|
|
# These two fields can go away when we're not 1:1 with SolitudeSellers.
|
|
seller_uri = models.CharField(max_length=255, unique=True)
|
|
uri = models.CharField(max_length=255, unique=True)
|
|
# A soft-delete so we can talk to Solitude asynchronously.
|
|
inactive = models.BooleanField(default=False)
|
|
bango_package_id = models.IntegerField(blank=True, null=True)
|
|
|
|
BANGO_PACKAGE_VALUES = (
|
|
'adminEmailAddress', 'supportEmailAddress', 'financeEmailAddress',
|
|
'paypalEmailAddress', 'vendorName', 'companyName', 'address1',
|
|
'addressCity', 'addressState', 'addressZipCode', 'addressPhone',
|
|
'countryIso', 'currencyIso', 'vatNumber')
|
|
BANGO_BANK_DETAILS_VALUES = (
|
|
'seller_bango', 'bankAccountPayeeName', 'bankAccountNumber',
|
|
'bankAccountCode', 'bankName', 'bankAddress1', 'bankAddressZipCode',
|
|
'bankAddressIso', )
|
|
|
|
class Meta:
|
|
db_table = 'payment_accounts'
|
|
unique_together = ('user', 'uri')
|
|
|
|
@classmethod
|
|
def create_bango(cls, user, form_data):
|
|
# Get the seller object.
|
|
user_seller = SolitudeSeller.create(user)
|
|
|
|
# Get the data together for the package creation.
|
|
package_values = dict((k, v) for k, v in form_data.items() if
|
|
k in cls.BANGO_PACKAGE_VALUES)
|
|
# Dummy value since we don't really use this.
|
|
package_values.setdefault('paypalEmailAddress', 'nobody@example.com')
|
|
package_values['seller'] = user_seller.resource_uri
|
|
|
|
log.info('[User:%s] Creating Bango package' % user)
|
|
res = client.api.bango.package.post(data=package_values)
|
|
uri = res['resource_uri']
|
|
|
|
# Get the data together for the bank details creation.
|
|
bank_details_values = dict((k, v) for k, v in form_data.items() if
|
|
k in cls.BANGO_BANK_DETAILS_VALUES)
|
|
bank_details_values['seller_bango'] = uri
|
|
|
|
log.info('[User:%s] Creating Bango bank details' % user)
|
|
client.api.bango.bank.post(data=bank_details_values)
|
|
|
|
obj = cls.objects.create(user=user, uri=uri,
|
|
solitude_seller=user_seller,
|
|
seller_uri=user_seller.resource_uri,
|
|
bango_package_id=res['package_id'],
|
|
name=form_data['account_name'])
|
|
|
|
log.info('[User:%s] Created Bango payment account (uri: %s)' %
|
|
(user, uri))
|
|
return obj
|
|
|
|
def cancel(self, disable_refs=False):
|
|
"""Cancels the payment account.
|
|
|
|
If `disable_refs` is set, existing apps that use this payment account
|
|
will be set to STATUS_NULL.
|
|
|
|
"""
|
|
self.update(inactive=True)
|
|
log.info('[1@None] Soft-deleted payment account (uri: %s)' %
|
|
self.uri)
|
|
|
|
account_refs = AddonPaymentAccount.objects.filter(account_uri=self.uri)
|
|
for acc_ref in account_refs:
|
|
if disable_refs:
|
|
acc_ref.addon.update(status=amo.STATUS_NULL)
|
|
acc_ref.delete()
|
|
|
|
def update_account_details(self, **kwargs):
|
|
self.update(name=kwargs.pop('account_name'))
|
|
client.api.by_url(self.uri).patch(
|
|
data=dict((k, v) for k, v in kwargs.items() if
|
|
k in self.BANGO_PACKAGE_VALUES))
|
|
|
|
def get_details(self):
|
|
data = {'account_name': self.name}
|
|
package_data = (client.api.bango.package(uri_to_pk(self.uri))
|
|
.get(data={'full': True}))
|
|
data.update((k, v) for k, v in package_data.get('full').items() if
|
|
k in self.BANGO_PACKAGE_VALUES)
|
|
return data
|
|
|
|
def __unicode__(self):
|
|
return u'%s - %s' % (self.created.strftime('%m/%y'), self.name)
|
|
|
|
def get_agreement_url(self):
|
|
return reverse('mkt.developers.bango.agreement', args=[self.pk])
|
|
|
|
|
|
class AddonPaymentAccount(amo.models.ModelBase):
|
|
addon = models.OneToOneField(
|
|
'addons.Addon', related_name='app_payment_account')
|
|
payment_account = models.ForeignKey(PaymentAccount)
|
|
provider = models.CharField(
|
|
max_length=8, choices=[('bango', _lazy('Bango'))])
|
|
account_uri = models.CharField(max_length=255)
|
|
product_uri = models.CharField(max_length=255, unique=True)
|
|
|
|
class Meta:
|
|
db_table = 'addon_payment_account'
|
|
|
|
@classmethod
|
|
def create(cls, provider, addon, payment_account):
|
|
# TODO: remove once API is the only access point.
|
|
uri = cls.setup_bango(provider, addon, payment_account)
|
|
return cls.objects.create(addon=addon, provider=provider,
|
|
payment_account=payment_account,
|
|
account_uri=payment_account.uri,
|
|
product_uri=uri)
|
|
|
|
@classmethod
|
|
def setup_bango(cls, provider, addon, payment_account):
|
|
secret = generate_key(48)
|
|
external_id = webpay.make_ext_id(addon.pk)
|
|
data = {'seller': uri_to_pk(payment_account.seller_uri),
|
|
'external_id': external_id}
|
|
try:
|
|
generic_product = client.api.generic.product.get_object(**data)
|
|
except ObjectDoesNotExist:
|
|
generic_product = client.api.generic.product.post(data={
|
|
'seller': payment_account.seller_uri, 'secret': secret,
|
|
'external_id': external_id, 'public_id': str(uuid.uuid4()),
|
|
'access': ACCESS_PURCHASE,
|
|
})
|
|
|
|
product_uri = generic_product['resource_uri']
|
|
if provider == 'bango':
|
|
uri = cls._create_bango(
|
|
product_uri, addon, payment_account, secret)
|
|
else:
|
|
uri = ''
|
|
return uri
|
|
|
|
@classmethod
|
|
def _create_bango(cls, product_uri, addon, payment_account, secret):
|
|
if not payment_account.bango_package_id:
|
|
raise NotImplementedError('Currently we only support Bango '
|
|
'so the associated account must '
|
|
'have a bango_package_id')
|
|
res = None
|
|
if product_uri:
|
|
data = {'seller_product': uri_to_pk(product_uri)}
|
|
try:
|
|
res = client.api.bango.product.get_object(**data)
|
|
except ObjectDoesNotExist:
|
|
# The product does not exist in Solitude so create it.
|
|
res = client.api.bango.product.post(data={
|
|
'seller_bango': payment_account.uri,
|
|
'seller_product': product_uri,
|
|
'name': unicode(addon.name),
|
|
'packageId': payment_account.bango_package_id,
|
|
'categoryId': 1,
|
|
'secret': secret
|
|
})
|
|
|
|
product_uri = res['resource_uri']
|
|
bango_number = res['bango_id']
|
|
|
|
# If the app is already premium this does nothing.
|
|
cls._push_bango_premium(
|
|
bango_number, product_uri, float(addon.addonpremium.price.price))
|
|
|
|
return product_uri
|
|
|
|
@classmethod
|
|
def _push_bango_premium(cls, bango_number, product_uri, price):
|
|
if price != 0:
|
|
# Make the Bango product premium.
|
|
client.api.bango.premium.post(
|
|
data={'bango': bango_number,
|
|
'price': price,
|
|
'currencyIso': 'USD',
|
|
'seller_product_bango': product_uri})
|
|
|
|
# Update the Bango rating.
|
|
client.api.bango.rating.post(
|
|
data={'bango': bango_number,
|
|
'rating': 'UNIVERSAL',
|
|
'ratingScheme': 'GLOBAL',
|
|
'seller_product_bango': product_uri})
|
|
# Bug 836865.
|
|
client.api.bango.rating.post(
|
|
data={'bango': bango_number,
|
|
'rating': 'GENERAL',
|
|
'ratingScheme': 'USA',
|
|
'seller_product_bango': product_uri})
|
|
|
|
return product_uri
|
|
|
|
def update_price(self, new_price):
|
|
if self.provider == 'bango':
|
|
# Get the Bango number for this product.
|
|
res = client.api.by_url(self.product_uri).get_object()
|
|
bango_number = res['bango_id']
|
|
|
|
AddonPaymentAccount._push_bango_premium(
|
|
bango_number, self.product_uri, new_price)
|
|
|
|
def delete(self):
|
|
if self.provider == 'bango':
|
|
# TODO(solitude): Once solitude supports DeleteBangoNumber, that
|
|
# goes here.
|
|
# ...also, make it a (celery) task.
|
|
|
|
# client.delete_product_bango(self.product_uri)
|
|
pass
|
|
|
|
super(AddonPaymentAccount, self).delete()
|
|
|
|
|
|
class UserInappKey(amo.models.ModelBase):
|
|
solitude_seller = models.ForeignKey(SolitudeSeller)
|
|
seller_product_pk = models.IntegerField(unique=True)
|
|
|
|
def secret(self):
|
|
return self._product().get()['secret']
|
|
|
|
def public_id(self):
|
|
return self._product().get()['public_id']
|
|
|
|
def reset(self):
|
|
self._product().patch(data={'secret': generate_key(48)})
|
|
|
|
@classmethod
|
|
def create(cls, user):
|
|
sel = SolitudeSeller.create(user)
|
|
# Create a product key that can only be used for simulated purchases.
|
|
prod = client.api.generic.product.post(data={
|
|
'seller': sel.resource_uri, 'secret': generate_key(48),
|
|
'external_id': str(uuid.uuid4()), 'public_id': str(uuid.uuid4()),
|
|
'access': ACCESS_SIMULATE,
|
|
})
|
|
log.info('User %s created an in-app payments dev key product=%s '
|
|
'with %s' % (user, prod['resource_pk'], sel))
|
|
return cls.objects.create(solitude_seller=sel,
|
|
seller_product_pk=prod['resource_pk'])
|
|
|
|
def _product(self):
|
|
return client.api.generic.product(self.seller_product_pk)
|
|
|
|
class Meta:
|
|
db_table = 'user_inapp_keys'
|