diff --git a/docs/api/topics/apps.rst b/docs/api/topics/apps.rst index 683e3e24f4..320eacf40a 100644 --- a/docs/api/topics/apps.rst +++ b/docs/api/topics/apps.rst @@ -180,10 +180,15 @@ App refer to Android mobile and tablet. As opposed to Firefox OS. :param required premium_type: One of `free`, `premium`, `free-inapp`, `premium-inapp`, or `other`. + :param optional price: The price for your app as a string, for example + "0.10". Required for `premium` or `premium-inapp` apps. + :param optional payment_account: The path for the + :ref:`payment account ` resource you want to + associate with this app. **Response** - :status 201: successfully updated. + :status 202: successfully updated. .. http:delete:: /api/v1/apps/app/(int:id)/ diff --git a/docs/api/topics/payment.rst b/docs/api/topics/payment.rst index 547ec33c33..32996e916f 100644 --- a/docs/api/topics/payment.rst +++ b/docs/api/topics/payment.rst @@ -7,6 +7,8 @@ Payments This API is specific to setting up and processing payments for an app in the Marketplace. +.. _payment-account-label: + Configuring payment accounts ============================ diff --git a/mkt/api/resources.py b/mkt/api/resources.py index 8b7ea5cb08..21801deb6c 100644 --- a/mkt/api/resources.py +++ b/mkt/api/resources.py @@ -25,6 +25,7 @@ from amo.utils import no_translation from constants.applications import DEVICE_TYPES from files.models import FileUpload, Platform from lib.metrics import record_action +from market.models import AddonPremium, Price from mkt.api.authentication import (OptionalOAuthAuthentication, OAuthAuthentication) @@ -38,6 +39,7 @@ from mkt.api.http import HttpLegallyUnavailable from mkt.carriers import get_carrier_id, CARRIERS, CARRIER_MAP from mkt.developers import tasks from mkt.developers.forms import NewManifestForm, PreviewForm +from mkt.developers.models import AddonPaymentAccount from mkt.regions import get_region_id, get_region, REGIONS_DICT from mkt.submit.forms import AppDetailsBasicForm from mkt.webapps.utils import app_to_dict @@ -109,14 +111,17 @@ class ValidationResource(CORSResource, MarketplaceModelResource): class AppResource(CORSResource, MarketplaceModelResource): + payment_account = fields.ToOneField('mkt.developers.api.AccountResource', + 'app_payment_account', null=True) + premium_type = fields.IntegerField(null=True) previews = fields.ToManyField('mkt.api.resources.PreviewResource', 'previews', readonly=True) - premium_type = fields.IntegerField() class Meta(MarketplaceModelResource.Meta): queryset = Webapp.objects.all() # Gets overriden in dispatch. fields = ['categories', 'description', 'device_types', 'homepage', - 'id', 'name', 'premium_type', 'privacy_policy', + 'id', 'name', 'payment_account', 'premium_type', + 'privacy_policy', 'status', 'summary', 'support_email', 'support_url'] list_allowed_methods = ['get', 'post'] detail_allowed_methods = ['get', 'put', 'delete'] @@ -241,7 +246,8 @@ class AppResource(CORSResource, MarketplaceModelResource): data['app_slug'] = data.get('app_slug', obj.app_slug) data.update(self.formset(data)) data.update(self.devices(data)) - self.hydrate_premium_type(bundle) + self.update_premium_type(bundle) + self.update_payment_account(bundle) forms = [AppDetailsBasicForm(data, instance=obj, request=request), DeviceTypeForm(data, addon=obj), @@ -258,6 +264,45 @@ class AppResource(CORSResource, MarketplaceModelResource): return bundle + def update_premium_type(self, bundle): + self.hydrate_premium_type(bundle) + if bundle.obj.premium_type != amo.ADDON_FREE: + ap = AddonPremium.objects.safer_get_or_create(addon=bundle.obj)[0] + else: + ap = None + if bundle.obj.premium_type in amo.ADDON_PREMIUMS: + if not bundle.data.get('price') or not Price.objects.filter( + price=bundle.data['price']).exists(): + tiers = ', '.join('"%s"' % p.get_price() + for p in Price.objects.exclude(price="0.00")) + raise fields.ApiFieldError( + 'Premium app specified without a valid price. price can be' + ' one of %s.' % (tiers,)) + else: + ap.price = Price.objects.get(price=bundle.data['price']) + ap.save() + else: + if ap: + ap.price = Price.objects.get(price='0.00') + ap.save() + + def update_payment_account(self, bundle): + if 'payment_account' in bundle.data: + if bundle.obj.premium_type == amo.ADDON_FREE: + raise fields.ApiFieldError( + 'Free apps cannot have payment accounts.') + acct = self.fields['payment_account'].hydrate(bundle).obj + try: + log.info('[1@%s] Deleting app payment account' % bundle.obj.pk) + AddonPaymentAccount.objects.get(addon=bundle.obj).delete() + except AddonPaymentAccount.DoesNotExist: + pass + + log.info('[1@%s] Creating new app payment account' % bundle.obj.pk) + AddonPaymentAccount.create( + provider='bango', addon=bundle.obj, + payment_account=acct) + def dehydrate(self, bundle): obj = bundle.obj user = getattr(bundle.request, 'user', None) diff --git a/mkt/api/tests/test_handlers.py b/mkt/api/tests/test_handlers.py index e9c99e1232..01c97fc3d2 100644 --- a/mkt/api/tests/test_handlers.py +++ b/mkt/api/tests/test_handlers.py @@ -11,11 +11,15 @@ from addons.models import (Addon, AddonDeviceType, AddonUpsell, AddonUser, Category, Flag, Preview) from amo.tests import AMOPaths, app_factory from files.models import FileUpload +from market.models import Price, AddonPremium from users.models import UserProfile -from mkt.api.tests.test_oauth import BaseOAuth +from mkt.api.tests.test_oauth import BaseOAuth, get_absolute_url from mkt.api.base import get_url, list_url from mkt.constants import APP_IMAGE_SIZES, carriers, regions +from mkt.developers.models import (AddonPaymentAccount, PaymentAccount, + SolitudeSeller) +from mkt.developers.tests.test_api import payment_data from mkt.site.fixtures import fixture from mkt.webapps.models import ContentRating, ImageAsset, Webapp from reviews.models import Review @@ -533,6 +537,130 @@ class TestAppCreateHandler(CreateHandler, AMOPaths): assert '12345' in self.get_error(res)['device_types'][0], ( self.get_error(res)) + def test_put_price(self): + app = self.create_app() + data = self.base_data() + Price.objects.create(price='1.07') + data['premium_type'] = 'premium' + data['price'] = "1.07" + res = self.client.put(self.get_url, data=json.dumps(data)) + eq_(res.status_code, 202) + eq_(str(app.addonpremium.price.get_price()), "1.07") + + def test_put_premium_inapp(self): + app = self.create_app() + data = self.base_data() + Price.objects.create(price='1.07') + data['premium_type'] = 'premium-inapp' + data['price'] = "1.07" + res = self.client.put(self.get_url, data=json.dumps(data)) + eq_(res.status_code, 202) + eq_(str(app.addonpremium.price.get_price()), "1.07") + eq_(Webapp.objects.get(pk=app.pk).premium_type, + amo.ADDON_PREMIUM_INAPP) + + def test_put_bad_price(self): + self.create_app() + data = self.base_data() + Price.objects.create(price='1.07') + Price.objects.create(price='3.14') + data['premium_type'] = 'premium' + data['price'] = "2.03" + res = self.client.put(self.get_url, data=json.dumps(data)) + eq_(res.status_code, 400) + eq_(res.content, + 'Premium app specified without a valid price. price can be one of ' + '"1.07", "3.14".') + + def test_put_no_price(self): + self.create_app() + data = self.base_data() + Price.objects.create(price='1.07') + Price.objects.create(price='3.14') + data['premium_type'] = 'premium' + res = self.client.put(self.get_url, data=json.dumps(data)) + eq_(res.status_code, 400) + eq_(res.content, + 'Premium app specified without a valid price. price can be one of ' + '"1.07", "3.14".') + + def test_put_free_inapp(self): + app = self.create_app() + data = self.base_data() + Price.objects.create(price='0.00') + Price.objects.create(price='3.14') + data['premium_type'] = 'free-inapp' + res = self.client.put(self.get_url, data=json.dumps(data)) + eq_(res.status_code, 202) + eq_(str(app.addonpremium.get_price()), "0.00") + + @patch('mkt.developers.models.client') + def test_get_payment_account(self, client): + client.api.bango.package().get.return_value = {"full": payment_data} + app = self.create_app() + app.premium_type = amo.ADDON_PREMIUM + app.save() + seller = SolitudeSeller.objects.create(user=self.profile, uuid='uid') + acct = PaymentAccount.objects.create( + user=self.profile, solitude_seller=seller, agreed_tos=True, + seller_uri='uri', uri='uri', bango_package_id=123) + AddonPaymentAccount.objects.create( + addon=app, payment_account=acct, set_price=1, + product_uri="/path/to/app/") + p = Price.objects.create(price='1.07') + AddonPremium.objects.create(addon=app, price=p) + acct_url = get_absolute_url(get_url('account', acct.pk), + 'payments', False) + res = self.client.get(self.get_url) + eq_(res.status_code, 200) + data = json.loads(res.content) + eq_(data['payment_account'], acct_url) + eq_(data['price'], '1.07') + + @patch('mkt.developers.models.client') + def test_put_payment_account(self, client): + client.api.bango.package().get.return_value = {"full": payment_data} + app = self.create_app() + data = self.base_data() + seller = SolitudeSeller.objects.create(user=self.profile, uuid='uid') + acct = PaymentAccount.objects.create( + user=self.profile, solitude_seller=seller, agreed_tos=True, + seller_uri='uri', uri='uri', bango_package_id=123) + Price.objects.create(price='1.07') + data['price'] = "1.07" + data['premium_type'] = 'premium' + data['payment_account'] = get_absolute_url(get_url('account', acct.pk), + 'payments', False) + res = self.client.put(self.get_url, data=json.dumps(data)) + eq_(res.status_code, 202) + eq_(app.app_payment_account.payment_account.pk, acct.pk) + + @patch('mkt.developers.models.client') + def test_put_payment_account_on_free(self, client): + client.api.bango.package().get.return_value = {"full": payment_data} + self.create_app() + data = self.base_data() + seller = SolitudeSeller.objects.create(user=self.profile, uuid='uid') + acct = PaymentAccount.objects.create( + user=self.profile, solitude_seller=seller, agreed_tos=True, + seller_uri='uri', uri='uri', bango_package_id=123) + data['payment_account'] = get_absolute_url(get_url('account', acct.pk), + 'payments', False) + res = self.client.put(self.get_url, data=json.dumps(data)) + eq_(res.status_code, 400) + + def test_put_bogus_payment_account(self): + app = self.create_app() + data = self.base_data() + Price.objects.create(price='1.07') + data['price'] = "1.07" + data['premium_type'] = 'premium' + data['payment_account'] = get_absolute_url(get_url('account', 999), + 'payments', False) + res = self.client.put(self.get_url, data=json.dumps(data)) + eq_(res.status_code, 400) + assert not hasattr(app, 'app_payment_account') + def test_put_not_mine(self): obj = self.create_app() obj.authors.clear() diff --git a/mkt/developers/tests/test_api.py b/mkt/developers/tests/test_api.py index d7e9a03da9..3dedde765e 100644 --- a/mkt/developers/tests/test_api.py +++ b/mkt/developers/tests/test_api.py @@ -119,6 +119,10 @@ class AccountTests(BaseOAuth): pkg['resource_uri'] = '/api/v1/payments/account/%s/' % self.account.pk eq_(data, pkg) + def test_only_get_by_owner(self, client): + r = self.anon.get(get_url('account', self.account.pk)) + eq_(r.status_code, 401) + def test_put(self, client): addr = 'b@b.com' newpkg = package_data.copy() diff --git a/mkt/webapps/utils.py b/mkt/webapps/utils.py index 397515bce5..21c38e1b94 100644 --- a/mkt/webapps/utils.py +++ b/mkt/webapps/utils.py @@ -40,6 +40,9 @@ def app_to_dict(app, currency=None, user=None): """Return app data as dict for API.""" # Sad circular import issues. from mkt.api.resources import PreviewResource + from mkt.developers.api import AccountResource + from mkt.developers.models import AddonPaymentAccount + cv = app.current_version version_data = { @@ -75,6 +78,10 @@ def app_to_dict(app, currency=None, user=None): } if app.premium: + q = AddonPaymentAccount.objects.filter(addon=app) + if len(q) > 0 and q[0].payment_account: + data['payment_account'] = AccountResource().get_resource_uri( + q[0].payment_account) try: data['price'] = app.get_price(currency) data['price_locale'] = app.get_price_locale(currency)