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'