Skip to content
This repository has been archived by the owner on Mar 15, 2018. It is now read-only.

Commit

Permalink
Merge pull request #863 from muffinresearch/split-free-inapp-and-tier…
Browse files Browse the repository at this point in the history
…-0-887994

Make free with in_app separate to price tier 0 (bug 887994)
  • Loading branch information
muffinresearch committed Jul 2, 2013
2 parents 9968e38 + db1064a commit 1b0d9ae
Show file tree
Hide file tree
Showing 8 changed files with 269 additions and 86 deletions.
43 changes: 23 additions & 20 deletions media/js/devreg/payments.js
Expand Up @@ -7,7 +7,7 @@ define('payments', [], function() {

var apiErrorMsg = $regions.data('apiErrorMsg');
var disabledRegions = $regions.data('disabledRegions');
var freeWithInAppId = $regions.data('freeWithInappId');
var tierZeroId = $regions.data('tierZeroId');
var notApplicableMsg = $regions.data('notApplicableMsg');
var paymentMethods = $regions.data('paymentMethods') || {};
var pricesApiEndpoint = $regions.data('pricelistApiUrl') + '{0}/';
Expand Down Expand Up @@ -92,38 +92,50 @@ define('payments', [], function() {

function updatePrices() {
/*jshint validthis:true */
var apiUrl;
var $this = $(this);
var selectedPrice = parseInt($this.val(), 10);
var selectedPrice = $this.val();

// Check for NaN which will be caused by selectedPrice being ''.
selectedPrice = isNaN(selectedPrice) ? false : selectedPrice;
if (selectedPrice != 'free') {
selectedPrice = parseInt(selectedPrice, 10);
selectedPrice = isNaN(selectedPrice) ? false : selectedPrice;
}

// No-op if nothing has changed.
if (currentPrice === selectedPrice) {
return;
}

// Handle the 'Please select a price' case.
if (selectedPrice === false) {
$regions.find('input[type=checkbox]').each(disableCheckbox);
currentPrice = selectedPrice;
return;
}

apiUrl = format(pricesApiEndpoint, selectedPrice);

// If free with in-app is selected, check "Yes" then make the 'No' radio
// disabled and hide it.
if (selectedPrice === freeWithInAppId) {
// disabled and hide it. Also hide upsell as that's not relevant.
if (selectedPrice == 'free') {
$('input[name=allow_inapp][value=True]').prop('checked', true);
$('input[name=allow_inapp][value=False]').prop('disabled', true)
.parent('label').hide();

// Enable all the checkboxes.
$regions.find('input[type=checkbox]').prop('disabled', false)
.closest('label').removeClass('disabled')
.closest('tr').find('.local-method, .local-retail')
.text(notApplicableMsg);
$('#paid-upsell-island').hide();
currentPrice = selectedPrice;
return;
} else {
$('#paid-upsell-island').show();
$('input[name=allow_inapp][value=False]').prop('disabled', false)
.parent('label').show();
}

$.ajax({
url: apiUrl,
url: format(pricesApiEndpoint, selectedPrice),
beforeSend: function() {
$regionsIsland.addClass('loading');
},
Expand All @@ -148,20 +160,11 @@ define('payments', [], function() {

var $tr = $chkbox.closest('tr');

// Display local currency for price.
$tr.find('.local-retail')
.text(price.price + ' ' + price.currency);

if (selectedPrice === freeWithInAppId) {
// Show not applicable as this is a free app with
// in-app payments.
$tr.find('.local-method')
.text(notApplicableMsg);
} else {
// Display local billing method.
$tr.find('.local-method')
.text(billingMethodText);
}
$tr.find('.local-method')
.text(selectedPrice === tierZeroId ? notApplicableMsg : billingMethodText);

seen.push($chkbox[0]);
}
Expand Down
10 changes: 8 additions & 2 deletions mkt/developers/forms.py
Expand Up @@ -41,6 +41,7 @@
from mkt.constants.ratingsbodies import (ALL_RATINGS, RATINGS_BODIES,
RATINGS_BY_NAME)
from mkt.site.forms import AddonChoiceField
from mkt.regions import ALL_PAID_REGION_IDS
from mkt.webapps.models import (AddonExcludedRegion, ContentRating, ImageAsset,
Webapp)

Expand Down Expand Up @@ -708,9 +709,13 @@ def __init__(self, *args, **kw):
self.fields['other_regions'].label = _(u'Other regions')

# Premium form was valid.
if self.price:
if self.price and self.price != 'free':
self.price_region_ids = (self.price.pricecurrency_set
.values_list('region', flat=True))
# Free app with in-app payments for this we just want to make
# sure it's in a region that allows payments.
elif self.price and self.price == 'free':
self.price_region_ids = ALL_PAID_REGION_IDS
# Premium form wasn't valid and it is a POST. Since we can't
# determine what price they wanted, just make sure it isn't a
# disabled price.
Expand All @@ -730,7 +735,8 @@ def is_toggling(self):
return value if value in ('free', 'paid') else False

def _product_is_paid(self):
return self.product.premium_type in amo.ADDON_PREMIUMS
return (self.product.premium_type in amo.ADDON_PREMIUMS
or self.product.premium_type == amo.ADDON_FREE_INAPP)

def has_inappropriate_regions(self):
"""Returns whether the app is listed in regions that it shouldn't
Expand Down
121 changes: 78 additions & 43 deletions mkt/developers/forms_payments.py
Expand Up @@ -63,14 +63,16 @@ def __init__(self, *args, **kw):
kw['initial'] = {
'allow_inapp': self.addon.premium_type in amo.ADDON_INAPPS
}
if self.addon.premium and self.addon.premium.price:

if self.addon.premium_type == amo.ADDON_FREE_INAPP:
kw['initial']['price'] = 'free'
elif self.addon.premium and self.addon.premium.price:
# If the app has a premium object, set the initial price.
kw['initial']['price'] = self.addon.premium.price.pk

super(PremiumForm, self).__init__(*args, **kw)

if (self.addon.premium_type in amo.ADDON_PREMIUMS
and not self.is_toggling()):
if (self.is_paid() and not self.is_toggling()):
# Require the price field if the app is premium and
# we're not toggling from free <-> paid.
self.fields['price'].required = True
Expand Down Expand Up @@ -99,17 +101,20 @@ def __init__(self, *args, **kw):

def group_tier_choices(self):
"""Creates tier choices with optgroups based on payment methods"""
price_choices = [('', _('Please select a price'))]
price_choices = [
('', _('Please select a price')),
('free', _('Free (with in-app payments)')),
]
card_billed = []
operator_billed = []
card_and_operator_billed = []

for price in Price.objects.active():
choice = (price.pk, unicode(price))
# Special case zero priced tier.
# Special case price tier 0.
if price.price == Decimal('0.00'):
price_choices.append((price.pk,
_lazy('Free with in-app payments')))
price_choices.append((price.pk, '%s (%s)' %
(unicode(price), _('Promotional Pricing'))))
# Tiers that can only be operator billed.
elif price.method == PAYMENT_METHOD_OPERATOR:
operator_billed.append(choice)
Expand Down Expand Up @@ -151,7 +156,12 @@ def _make_premium(self):
price_id=self._initial_price_id())

def is_paid(self):
return self.addon.premium_type in amo.ADDON_PREMIUMS
is_paid = (self.addon.premium_type in amo.ADDON_PREMIUMS
or self.is_free_inapp())
return is_paid

def is_free_inapp(self):
return self.addon.premium_type == amo.ADDON_FREE_INAPP

def is_toggling(self):
value = self.request.POST.get('toggle-paid')
Expand Down Expand Up @@ -187,45 +197,51 @@ def clean(self):

def clean_price(self):

price_id = self.cleaned_data['price']
if (self.cleaned_data.get('premium_type') in amo.ADDON_PREMIUMS
and not price_id and not self.is_toggling()):
price_value = self.cleaned_data.get('price')
premium_type = self.cleaned_data.get('premium_type')
if ((premium_type in amo.ADDON_PREMIUMS
or premium_type == amo.ADDON_FREE_INAPP)
and not price_value and not self.is_toggling()):
raise_required()

if not price_id and self.fields['price'].required is False:
if not price_value and self.fields['price'].required is False:
return None

# Special case for a free app - in-app payments must be enabled.
# Note: this isn't enforced for tier zero apps.
if price_value == 'free':
if self.cleaned_data.get('allow_inapp') != 'True':
raise ValidationError(_('If app is Free, '
'in-app payments must be enabled'))
return price_value

try:
price = Price.objects.get(pk=price_id, active=True)
price = Price.objects.get(pk=price_value, active=True)
except (ValueError, Price.DoesNotExist):
raise ValidationError(
self.fields['price'].error_messages['invalid_choice'])

if (price and price.price == Decimal('0.00')
and self.cleaned_data.get('allow_inapp') != 'True'):
raise ValidationError(_('If app is Free, '
'in-app payments must be enabled'))
raise ValidationError(_('Not a valid choice'))

return price

def save(self):
toggle = self.is_toggling()
upsell = self.addon.upsold
is_premium = self.is_paid()

# is_paid is true for both premium apps and free apps with
# in-app payments.
is_paid = self.is_paid()

if toggle == 'paid' and self.addon.premium_type == amo.ADDON_FREE:
# Toggle free apps to paid by giving them a premium object.

premium = self._make_premium()
premium.price_id = self._initial_price_id()
premium.save()

self.addon.premium_type = amo.ADDON_PREMIUM
self.addon.status = amo.STATUS_NULL

is_premium = True
is_paid = True

elif toggle == 'free' and is_premium:
elif toggle == 'free' and is_paid:
# If the app is paid and we're making it free, remove it as an
# upsell (if an upsell exists).
upsell = self.addon.upsold
Expand All @@ -244,29 +260,48 @@ def save(self):
if self.addon.status == amo.STATUS_NULL:
_restore_app(self.addon, save=False)

is_premium = False

elif is_premium:
# The dev is submitting updates for payment data about a paid app.
# This might also happen if she is associating a new paid app
# with an existing bank account.
premium = self._make_premium()
self.addon.premium_type = (
amo.ADDON_PREMIUM_INAPP if
self.cleaned_data.get('allow_inapp') == 'True' else
amo.ADDON_PREMIUM)

if 'price' in self.cleaned_data:
log.debug('[1@%s] Updating app price (%s)' %
(self.addon.pk, self.cleaned_data['price']))
premium.price = self.cleaned_data['price']

premium.save()
is_paid = False

# Right is_paid is both paid apps and free with in-app payments.
elif is_paid:
price = self.cleaned_data.get('price')

# If price is free then we want to make this an app that's
# free with in-app payments.
if price == 'free':
self.addon.premium_type = amo.ADDON_FREE_INAPP
log.debug('[1@%s] Changing to free with in_app' % self.addon.pk)

# Remove upsell
upsell = self.addon.upsold
if upsell:
log.debug('[1@%s] Removing upsell; switching to free '
'with in_app' % self.addon.pk)
upsell.delete()

if self.addon.status == amo.STATUS_NULL:
_restore_app(self.addon, save=False)
else:
# The dev is submitting updates for payment data about a paid
# app. This might also happen if he/she is associating a new
# paid app with an existing bank account.
premium = self._make_premium()
self.addon.premium_type = (
amo.ADDON_PREMIUM_INAPP if
self.cleaned_data.get('allow_inapp') == 'True' else
amo.ADDON_PREMIUM)

if price and price != 'free':
log.debug('[1@%s] Updating app price (%s)' %
(self.addon.pk, self.cleaned_data['price']))
premium.price = self.cleaned_data['price']

premium.save()

if not toggle:
# Save the device compatibility information when we're not
# toggling.
super(PremiumForm, self).save(self.addon, is_premium)
super(PremiumForm, self).save(self.addon, is_paid)

log.info('Saving app payment changes for addon %s.' % self.addon.pk)
self.addon.save()
Expand Down
4 changes: 2 additions & 2 deletions mkt/developers/templates/developers/payments/premium.html
Expand Up @@ -176,10 +176,10 @@ <h2>{{ _('Prices and countries') }}</h2>
<div id="region-list" class="checkbox-choices regions"
data-api-error-msg="{{ _('A server error occurred. Please try again later.') }}"
data-disabled-regions="{{ region_form.disabled_regions|json }}"
data-free-with-inapp-id="{{ free_with_in_app_id }}"
data-not-applicable-msg="{{ _('Not applicable') }}"
data-payment-methods="{{ payment_methods|json }}"
data-pricelist-api-url="{{ api_pricelist_url }}">
data-pricelist-api-url="{{ api_pricelist_url }}"
data-tier-zero-id="{{ tier_zero_id }}">
{{ region_form.regions.errors }}
<table>
<thead>
Expand Down

0 comments on commit 1b0d9ae

Please sign in to comment.