This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/data/billing.py

289 lines
7 KiB
Python
Raw Normal View History

import stripe
from datetime import datetime, timedelta
from calendar import timegm
from util.morecollections import AttrDict
PLANS = [
# Deprecated Plans
{
'title': 'Micro',
'price': 700,
'privateRepos': 5,
'stripeId': 'micro',
'audience': 'For smaller teams',
'bus_features': False,
'deprecated': True,
2015-08-10 21:36:13 +00:00
'free_trial_days': 14,
'superseded_by': None,
},
{
'title': 'Basic',
'price': 1200,
'privateRepos': 10,
'stripeId': 'small',
'audience': 'For your basic team',
'bus_features': False,
'deprecated': True,
2015-08-10 21:36:13 +00:00
'free_trial_days': 14,
'superseded_by': None,
},
{
2015-08-10 21:36:13 +00:00
'title': 'Yacht',
'price': 5000,
'privateRepos': 20,
2015-08-10 21:36:13 +00:00
'stripeId': 'bus-coreos-trial',
'audience': 'For small businesses',
'bus_features': True,
'deprecated': True,
2015-08-10 21:36:13 +00:00
'free_trial_days': 180,
'superseded_by': 'bus-small-30',
},
{
2015-08-10 21:36:13 +00:00
'title': 'Personal',
'price': 1200,
'privateRepos': 5,
'stripeId': 'personal',
'audience': 'Individuals',
'bus_features': False,
'deprecated': True,
2015-08-10 21:36:13 +00:00
'free_trial_days': 14,
'superseded_by': 'personal-30',
2015-08-10 21:36:13 +00:00
},
{
'title': 'Skiff',
'price': 2500,
'privateRepos': 10,
'stripeId': 'bus-micro',
'audience': 'For startups',
'bus_features': True,
'deprecated': True,
'free_trial_days': 14,
'superseded_by': 'bus-micro-30',
},
2014-08-13 18:25:48 +00:00
{
'title': 'Yacht',
'price': 5000,
'privateRepos': 20,
'stripeId': 'bus-small',
'audience': 'For small businesses',
'bus_features': True,
'deprecated': True,
2015-08-10 21:36:13 +00:00
'free_trial_days': 14,
'superseded_by': 'bus-small-30',
2015-08-10 21:36:13 +00:00
},
{
'title': 'Freighter',
'price': 10000,
'privateRepos': 50,
'stripeId': 'bus-medium',
'audience': 'For normal businesses',
'bus_features': True,
'deprecated': True,
'free_trial_days': 14,
'superseded_by': 'bus-medium-30',
2015-08-10 21:36:13 +00:00
},
{
'title': 'Tanker',
'price': 20000,
'privateRepos': 125,
'stripeId': 'bus-large',
'audience': 'For large businesses',
'bus_features': True,
'deprecated': True,
'free_trial_days': 14,
'superseded_by': 'bus-large-30',
2014-08-13 18:25:48 +00:00
},
# Active plans
{
'title': 'Open Source',
'price': 0,
'privateRepos': 0,
'stripeId': 'free',
'audience': 'Committment to FOSS',
'bus_features': False,
'deprecated': False,
2015-08-10 21:36:13 +00:00
'free_trial_days': 30,
'superseded_by': None,
},
{
'title': 'Personal',
'price': 1200,
'privateRepos': 5,
2015-08-10 21:36:13 +00:00
'stripeId': 'personal-30',
'audience': 'Individuals',
'bus_features': False,
'deprecated': False,
2015-08-10 21:36:13 +00:00
'free_trial_days': 30,
'superseded_by': None,
},
{
'title': 'Skiff',
'price': 2500,
'privateRepos': 10,
2015-08-10 21:36:13 +00:00
'stripeId': 'bus-micro-30',
'audience': 'For startups',
'bus_features': True,
'deprecated': False,
2015-08-10 21:36:13 +00:00
'free_trial_days': 30,
'superseded_by': None,
},
{
'title': 'Yacht',
'price': 5000,
'privateRepos': 20,
2015-08-10 21:36:13 +00:00
'stripeId': 'bus-small-30',
'audience': 'For small businesses',
'bus_features': True,
'deprecated': False,
2015-08-10 21:36:13 +00:00
'free_trial_days': 30,
'superseded_by': None,
},
{
'title': 'Freighter',
'price': 10000,
'privateRepos': 50,
2015-08-10 21:36:13 +00:00
'stripeId': 'bus-medium-30',
'audience': 'For normal businesses',
'bus_features': True,
'deprecated': False,
2015-08-10 21:36:13 +00:00
'free_trial_days': 30,
'superseded_by': None,
},
{
'title': 'Tanker',
'price': 20000,
'privateRepos': 125,
2015-08-10 21:36:13 +00:00
'stripeId': 'bus-large-30',
'audience': 'For large businesses',
'bus_features': True,
'deprecated': False,
2015-08-10 21:36:13 +00:00
'free_trial_days': 30,
'superseded_by': None,
},
]
def get_plan(plan_id):
""" Returns the plan with the given ID or None if none. """
for plan in PLANS:
if plan['stripeId'] == plan_id:
return plan
return None
class FakeStripe(object):
class Customer(AttrDict):
FAKE_PLAN = AttrDict({
'id': 'bus-small',
})
FAKE_SUBSCRIPTION = AttrDict({
'plan': FAKE_PLAN,
'current_period_start': timegm(datetime.utcnow().utctimetuple()),
'current_period_end': timegm((datetime.utcnow() + timedelta(days=30)).utctimetuple()),
'trial_start': timegm(datetime.utcnow().utctimetuple()),
'trial_end': timegm((datetime.utcnow() + timedelta(days=30)).utctimetuple()),
})
FAKE_CARD = AttrDict({
'id': 'card123',
'name': 'Joe User',
'type': 'Visa',
'last4': '4242',
'exp_month': 5,
'exp_year': 2016,
})
FAKE_CARD_LIST = AttrDict({
'data': [FAKE_CARD],
})
ACTIVE_CUSTOMERS = {}
@property
def card(self):
return self.get('new_card', None)
@card.setter
def card(self, card_token):
self['new_card'] = card_token
@property
def plan(self):
return self.get('new_plan', None)
@plan.setter
def plan(self, plan_name):
self['new_plan'] = plan_name
def save(self):
if self.get('new_card', None) is not None:
raise stripe.CardError('Test raising exception on set card.', self.get('new_card'), 402)
if self.get('new_plan', None) is not None:
if self.subscription is None:
self.subscription = AttrDict.deep_copy(self.FAKE_SUBSCRIPTION)
self.subscription.plan.id = self.get('new_plan')
if self.get('cancel_subscription', None) is not None:
self.subscription = None
def cancel_subscription(self):
self['cancel_subscription'] = True
@classmethod
def retrieve(cls, stripe_customer_id):
if stripe_customer_id in cls.ACTIVE_CUSTOMERS:
cls.ACTIVE_CUSTOMERS[stripe_customer_id].pop('new_card', None)
cls.ACTIVE_CUSTOMERS[stripe_customer_id].pop('new_plan', None)
cls.ACTIVE_CUSTOMERS[stripe_customer_id].pop('cancel_subscription', None)
return cls.ACTIVE_CUSTOMERS[stripe_customer_id]
else:
new_customer = cls({
'default_card': 'card123',
'cards': AttrDict.deep_copy(cls.FAKE_CARD_LIST),
'subscription': AttrDict.deep_copy(cls.FAKE_SUBSCRIPTION),
'id': stripe_customer_id,
})
cls.ACTIVE_CUSTOMERS[stripe_customer_id] = new_customer
return new_customer
class Invoice(AttrDict):
@staticmethod
def all(customer, count):
return AttrDict({
'data': [],
})
class Billing(object):
def __init__(self, app=None):
self.app = app
if app is not None:
self.state = self.init_app(app)
else:
self.state = None
def init_app(self, app):
billing_type = app.config.get('BILLING_TYPE', 'FakeStripe')
if billing_type == 'Stripe':
billing = stripe
stripe.api_key = app.config.get('STRIPE_SECRET_KEY', None)
elif billing_type == 'FakeStripe':
billing = FakeStripe
else:
raise RuntimeError('Unknown billing type: %s' % billing_type)
# register extension with app
app.extensions = getattr(app, 'extensions', {})
app.extensions['billing'] = billing
return billing
def __getattr__(self, name):
return getattr(self.state, name, None)