Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move to Stripe Prices #161

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
fail-fast: false
matrix:
python-version: ["3.10"]
thing-to-test: [flake8, 4.2, 5.0]
thing-to-test: [flake8, "4.2", "5.1"]

steps:
- uses: actions/checkout@v4
Expand Down
4 changes: 2 additions & 2 deletions bulk_lookup/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def get_form_kwargs(self, step):
if self.request.user.is_authenticated:
if not self.object:
self.object = self.get_object()
if self.object and self.object.plan.id == settings.PRICING[-1]['plan']:
if self.object and self.object.price.id == settings.PRICING[-1]['id']:
kwargs['free'] = True
return kwargs

Expand Down Expand Up @@ -108,7 +108,7 @@ def get_context_data(self, form, **kwargs):
context['num_good_rows'] = pc_data['num_rows'] - pc_data['bad_rows']
if self.steps.current == 'personal_details':
context['price'] = settings.BULK_LOOKUP_PRICE
if self.object and self.object.plan.id == settings.PRICING[-1]['plan']:
if self.object and self.object.price.id == settings.PRICING[-1]['id']:
context['price'] = 0
return context

Expand Down
7 changes: 7 additions & 0 deletions conf/general.yml-example
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ STRIPE_PUBLIC_KEY: ''
STRIPE_API_VERSION: ''
STRIPE_TAX_RATE: ''

PRICING_TIER_1_ID: 'price_123'
PRICING_TIER_2_ID: 'price_456'
PRICING_TIER_3_ID: 'price_789'
PRICING_TIER_1_AMOUNT: '20'
PRICING_TIER_2_AMOUNT: '100'
PRICING_TIER_3_AMOUNT: '300'

# Mapped to Django's DEBUG and TEMPLATE_DEBUG settings. Optional, defaults to True.
DEBUG: True

Expand Down
18 changes: 11 additions & 7 deletions mapit_mysociety_org/management/commands/add_mapit_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.conf import settings
from django.contrib.auth.models import User
from django.core.management.base import BaseCommand, CommandError
from django.utils.crypto import get_random_string
import stripe

from api_keys.models import APIKey
Expand All @@ -16,19 +17,22 @@ class Command(BaseCommand):
help = "Create a new user with associated Stripe subscription"

def add_arguments(self, parser):
plans = stripe.Plan.list()
plan_ids = [plan['id'] for plan in plans.data if plan['id'].startswith('mapit')]
prices = stripe.Price.list(limit=100, expand=['data.product'])
price_ids = [price.product.name for price in prices.data if price.product.name.startswith('MapIt')]
coupons = stripe.Coupon.list()
self.coupon_ids = [coupon['id'] for coupon in coupons if coupon['id'].startswith('charitable')]
parser.add_argument('--email', required=True)
parser.add_argument('--plan', choices=plan_ids, required=True)
parser.add_argument('--price', choices=price_ids, required=True)
parser.add_argument('--coupon', help='Existing coupons: ' + ', '.join(self.coupon_ids))
parser.add_argument('--trial', type=int)

def handle(self, *args, **options):
email = options['email']
coupon = options['coupon']
plan = options['plan']
price = options['price']

prices = stripe.Price.list(limit=100, expand=['data.product'])
price = [p for p in prices.data if p.product.name == price][0]

if coupon not in self.coupon_ids:
# coupon ID of the form charitableN(-Nmonths)
Expand All @@ -46,15 +50,15 @@ def handle(self, *args, **options):
)

username = email[:25]
password = User.objects.make_random_password(length=20)
password = get_random_string(20)
user = User.objects.create_user(username, email, password=password)
api_key = APIKey.objects.create(user=user, key=APIKey.generate_key())

customer = stripe.Customer.create(email=email).id
stripe_sub = stripe.Subscription.create(
customer=customer, plan=plan, coupon=coupon, trial_period_days=options['trial']).id
customer=customer, items=[{"price": price.id}], coupon=coupon, trial_period_days=options['trial']).id

sub = Subscription.objects.create(user=user, stripe_id=stripe_sub)
sub.redis_update_max(plan)
sub.redis_update_max(price)

self.stdout.write("Created user %s with password %s, API key %s\n" % (username, password, api_key.key))
17 changes: 12 additions & 5 deletions mapit_mysociety_org/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,15 @@ def allow_migrate(self, db, app_label, model_name=None, **hints):
BULK_LOOKUP_PRICE = 50

# API subscriptions
PRICING = [
{'plan': 'mapit-10k-v', 'price': 20, 'calls': '10,000'},
{'plan': 'mapit-100k-v', 'price': 100, 'calls': '100,000'},
{'plan': 'mapit-0k-v', 'price': 300, 'calls': '0'},
]
if 'test' in sys.argv:
PRICING = [
{'id': 'price_123', 'price': 20, 'calls': '10,000'},
{'id': 'price_456', 'price': 100, 'calls': '100,000'},
{'id': 'price_789', 'price': 300, 'calls': '0'},
]
else:
PRICING = [
{'id': config.get('PRICING_TIER_1_ID'), 'price': config.get('PRICING_TIER_1_AMOUNT'), 'calls': '10,000'},
{'id': config.get('PRICING_TIER_2_ID'), 'price': config.get('PRICING_TIER_2_AMOUNT'), 'calls': '100,000'},
{'id': config.get('PRICING_TIER_3_ID'), 'price': config.get('PRICING_TIER_3_AMOUNT'), 'calls': '0'},
]
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
{% include 'account/_form_field.html' with field=form.password_confirm %}
{% endif %}

{% include 'account/_form_field.html' with field=form.plan %}
<span id="js-price-information"
data-{{ PRICING.0.id }}="{{ PRICING.0.price }}"
data-{{ PRICING.1.id }}="{{ PRICING.1.price }}"
data-{{ PRICING.2.id }}="{{ PRICING.2.price }}"></span>

{% include 'account/_form_field.html' with field=form.price %}
{% include 'account/_form_field_checkbox.html' with field=form.charitable_tick %}
<div id="charitable-qns"{% if not form.charitable_tick.value %} style="display:none"{% endif %}>
{% include 'account/_form_field.html' with field=form.charitable %}
Expand Down
12 changes: 6 additions & 6 deletions mapit_mysociety_org/templates/pricing.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ <h2 class="pricing__option__cost">£{{ PRICING.0.price }}/mth</h2>
<li><strong>Free</strong> for charitable users</li>
</ul>
{% if request.user.is_authenticated %}
<a class="btn btn--primary" href="{% url 'subscription_update' %}?plan={{ PRICING.0.plan }}">Switch plan</a>
<a class="btn btn--primary" href="{% url 'subscription_update' %}?price={{ PRICING.0.id }}">Switch plan</a>
{% else %}
<a class="btn btn--primary" href="{% url 'account_signup' %}?plan={{ PRICING.0.plan }}">Sign up</a>
<a class="btn btn--primary" href="{% url 'account_signup' %}?price={{ PRICING.0.id }}">Sign up</a>
{% endif %}
</div>

Expand All @@ -51,9 +51,9 @@ <h2 class="pricing__option__cost">£{{ PRICING.1.price }}/mth</h2>
<li><strong>50% off</strong> for charitable users</li>
</ul>
{% if request.user.is_authenticated %}
<a class="btn btn--primary" href="{% url 'subscription_update' %}?plan={{ PRICING.1.plan }}">Switch plan</a>
<a class="btn btn--primary" href="{% url 'subscription_update' %}?price={{ PRICING.1.id }}">Switch plan</a>
{% else %}
<a class="btn btn--primary" href="{% url 'account_signup' %}?plan={{ PRICING.1.plan }}">Sign up</a>
<a class="btn btn--primary" href="{% url 'account_signup' %}?price={{ PRICING.1.id }}">Sign up</a>
{% endif %}
</div>

Expand All @@ -72,9 +72,9 @@ <h2 class="pricing__option__cost">£{{ PRICING.2.price }}/mth</h2>
<li><strong>50% off</strong> for charitable users</li>
</ul>
{% if request.user.is_authenticated %}
<a class="btn btn--primary" href="{% url 'subscription_update' %}?plan={{ PRICING.2.plan }}">Switch plan</a>
<a class="btn btn--primary" href="{% url 'subscription_update' %}?price={{ PRICING.2.id }}">Switch plan</a>
{% else %}
<a class="btn btn--primary" href="{% url 'account_signup' %}?plan={{ PRICING.2.plan }}">Sign up</a>
<a class="btn btn--primary" href="{% url 'account_signup' %}?price={{ PRICING.2.id }}">Sign up</a>
{% endif %}
</div>

Expand Down
23 changes: 17 additions & 6 deletions mapit_mysociety_org/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_signup(self, socket):
'password': 'password',
'password_confirm': 'password',
'tandcs_tick': 1,
'plan': 'mapit-10k-v',
'price': 'price_123',
'charitable_tick': 1,
'charitable': 'c',
'charity_number': '123',
Expand Down Expand Up @@ -59,7 +59,7 @@ def test_signup_card_error(self):
'password': 'password',
'password_confirm': 'password',
'tandcs_tick': 1,
'plan': 'mapit-10k-v',
'price': 'price_123',
'charitable_tick': 1,
'charitable': 'c',
'charity_number': '123',
Expand All @@ -71,18 +71,29 @@ def test_signup_card_error(self):
class ManagementTest(PatchedStripeMixin, PatchedRedisTestCase):
def test_add_mapit_user(self):
with patch('mapit_mysociety_org.management.commands.add_mapit_user.stripe', self.MockStripe):
self.MockStripe.Plan.list.return_value = convert_to_stripe_object({
'data': [{'id': 'mapit-0k-v'}, {'id': 'mapit-10k-v'}, {'id': 'mapit-100k-v'}]
self.MockStripe.Price.list.return_value = convert_to_stripe_object({
'data': [
{'id': 'price_789',
'metadata': {'calls': '0'},
'product': {'id': 'prod_GHI', 'name': 'MapIt, unlimited calls'}},
{'id': 'price_123',
'metadata': {'calls': '10000'},
'product': {'id': 'prod_ABC', 'name': 'MapIt, 10,000 calls'}},
{'id': 'price_456',
'metadata': {'calls': '100000'},
'product': {'id': 'prod_DEF', 'name': 'MapIt, 100,000 calls'}},
]
}, None, None)
call_command(
'add_mapit_user', '--email=[email protected]', '--plan=mapit-100k-v',
'add_mapit_user', '--email', '[email protected]', '--price', "MapIt, 100,000 calls",
coupon='charitable25-6months', trial='10',
stdout=StringIO(), stderr=StringIO())

self.MockStripe.Coupon.create.assert_called_once_with(
id='charitable25-6months', duration='repeating', duration_in_months='6', percent_off='25')
self.MockStripe.Subscription.create.assert_called_once_with(
customer='CUSTOMER-ID', plan='mapit-100k-v', coupon='charitable25-6months', trial_period_days='10')
customer='CUSTOMER-ID', items=[{"price": 'price_456'}],
coupon='charitable25-6months', trial_period_days='10')

user = User.objects.get(email='[email protected]')
sub = Subscription.objects.get(user=user)
Expand Down
2 changes: 1 addition & 1 deletion mapit_mysociety_org/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def dispatch(self, *args, **kwargs):

def get_initial(self):
initial = super(SignupView, self).get_initial()
initial['plan'] = self.request.GET.get('plan')
initial['price'] = self.request.GET.get('price')
return initial

def form_valid(self, form):
Expand Down
28 changes: 12 additions & 16 deletions static/js/payment.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
(function() {

function plan_cost() {
var plan = document.querySelector('input[name=plan]:checked'),
function price_cost() {
var price = document.querySelector('input[name=price]:checked'),
pricing = document.getElementById('js-price-information'),
ctick = document.getElementById('id_charitable_tick'),
c = document.querySelector('input[name=charitable]:checked');
plan = plan ? plan.value : '';
price = price ? price.value : '';
ctick = ctick ? ctick.checked : '';
c = c ? c.value : '';
var num = 20;
if (plan === 'mapit-100k-v') {
num = 100;
} else if (plan === 'mapit-0k-v') {
num = 300;
}
var num = pricing.dataset[price] || 20;
if (ctick) {
if (c === 'c' || c === 'i') {
if (num === 20) {
Expand All @@ -26,7 +22,7 @@ function plan_cost() {
}

function need_stripe() {
var num = plan_cost();
var num = price_cost();
if (num === 0 || document.getElementById('js-payment').getAttribute('data-has-payment-data')) {
return false;
}
Expand All @@ -43,10 +39,10 @@ function toggle_stripe() {
}
}

if (document.getElementById('id_plan_0')) {
document.getElementById('id_plan_0').addEventListener('change', toggle_stripe);
document.getElementById('id_plan_1').addEventListener('change', toggle_stripe);
document.getElementById('id_plan_2').addEventListener('change', toggle_stripe);
if (document.getElementById('id_price_0')) {
document.getElementById('id_price_0').addEventListener('change', toggle_stripe);
document.getElementById('id_price_1').addEventListener('change', toggle_stripe);
document.getElementById('id_price_2').addEventListener('change', toggle_stripe);
var opt = document.getElementById('charitable-desc').querySelector('.account-form__help_text');
document.getElementById('id_charitable_tick').addEventListener('click', function(e) {
if (this.checked) {
Expand Down Expand Up @@ -187,8 +183,8 @@ form && form.addEventListener('submit', function(e) {
errors += err('id_email');
errors += err('id_password');
errors += err('id_password_confirm');
var plan = document.querySelector('input[name=plan]:checked');
errors += err_highlight(document.querySelector('label[for=id_plan_0]'), !plan);
var price = document.querySelector('input[name=price]:checked');
errors += err_highlight(document.querySelector('label[for=id_price_0]'), !price);
var ctick = document.getElementById('id_charitable_tick').checked;
var c = document.querySelector('input[name=charitable]:checked');
errors += err_highlight(document.querySelector('label[for=id_charitable_0]'), ctick && !c);
Expand Down
12 changes: 6 additions & 6 deletions subscriptions/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@
from django.utils.safestring import mark_safe


def describe_plan(i):
def describe_price(i):
s = settings.PRICING[i]
if s['calls'] == '0':
desc = '£%d/mth - Unlimited calls' % s['price']
else:
desc = '£%d/mth - %s calls per month' % (s['price'], s['calls'])
return (s['plan'], desc)
return (s['id'], desc)


class SubscriptionMixin(forms.Form):
plan = forms.ChoiceField(choices=(
describe_plan(i) for i in range(3)
price = forms.ChoiceField(choices=(
describe_price(i) for i in range(3)
), label='Please choose a plan', widget=forms.RadioSelect)
charitable_tick = forms.BooleanField(
label='I qualify for a charitable discounted price',
Expand Down Expand Up @@ -61,8 +61,8 @@ def clean(self):

payment_data = cleaned_data.get('stripeToken') or cleaned_data.get('payment_method')
if not self.has_payment_data and not payment_data and not (
cleaned_data.get('plan') == settings.PRICING[0]['plan'] and typ in ('c', 'i')):
self.add_error('plan', 'You need to submit payment')
cleaned_data.get('price') == settings.PRICING[0]['id'] and typ in ('c', 'i')):
self.add_error('price', 'You need to submit payment')

if not self.stripe and not cleaned_data.get('tandcs_tick'):
self.add_error('tandcs_tick', 'Please agree to the terms and conditions')
Expand Down
Loading