import stripe from datetime import datetime, timedelta from calendar import timegm from util.morecollections import AttrDict PLANS = [ # Deprecated Plans (2013-2014) { 'title': 'Micro', 'price': 700, 'privateRepos': 5, 'stripeId': 'micro', 'audience': 'For smaller teams', 'bus_features': False, 'deprecated': True, 'free_trial_days': 14, 'superseded_by': 'personal-30', 'plans_page_hidden': False, }, { 'title': 'Basic', 'price': 1200, 'privateRepos': 10, 'stripeId': 'small', 'audience': 'For your basic team', 'bus_features': False, 'deprecated': True, 'free_trial_days': 14, 'superseded_by': 'bus-micro-30', 'plans_page_hidden': False, }, { 'title': 'Yacht', 'price': 5000, 'privateRepos': 20, 'stripeId': 'bus-coreos-trial', 'audience': 'For small businesses', 'bus_features': True, 'deprecated': True, 'free_trial_days': 180, 'superseded_by': 'bus-small-30', 'plans_page_hidden': False, }, { 'title': 'Personal', 'price': 1200, 'privateRepos': 5, 'stripeId': 'personal', 'audience': 'Individuals', 'bus_features': False, 'deprecated': True, 'free_trial_days': 14, 'superseded_by': 'personal-30', 'plans_page_hidden': False, }, { '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', 'plans_page_hidden': False, }, { 'title': 'Yacht', 'price': 5000, 'privateRepos': 20, 'stripeId': 'bus-small', 'audience': 'For small businesses', 'bus_features': True, 'deprecated': True, 'free_trial_days': 14, 'superseded_by': 'bus-small-30', 'plans_page_hidden': False, }, { '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', 'plans_page_hidden': False, }, { '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', 'plans_page_hidden': False, }, # Deprecated plans (2014-2017) { 'title': 'Personal', 'price': 1200, 'privateRepos': 5, 'stripeId': 'personal-30', 'audience': 'Individuals', 'bus_features': False, 'deprecated': True, 'free_trial_days': 30, 'superseded_by': 'personal-2018', 'plans_page_hidden': False, }, { 'title': 'Skiff', 'price': 2500, 'privateRepos': 10, 'stripeId': 'bus-micro-30', 'audience': 'For startups', 'bus_features': True, 'deprecated': True, 'free_trial_days': 30, 'superseded_by': 'bus-micro-2018', 'plans_page_hidden': False, }, { 'title': 'Yacht', 'price': 5000, 'privateRepos': 20, 'stripeId': 'bus-small-30', 'audience': 'For small businesses', 'bus_features': True, 'deprecated': True, 'free_trial_days': 30, 'superseded_by': 'bus-small-2018', 'plans_page_hidden': False, }, { 'title': 'Freighter', 'price': 10000, 'privateRepos': 50, 'stripeId': 'bus-medium-30', 'audience': 'For normal businesses', 'bus_features': True, 'deprecated': True, 'free_trial_days': 30, 'superseded_by': 'bus-medium-2018', 'plans_page_hidden': False, }, { 'title': 'Tanker', 'price': 20000, 'privateRepos': 125, 'stripeId': 'bus-large-30', 'audience': 'For large businesses', 'bus_features': True, 'deprecated': True, 'free_trial_days': 30, 'superseded_by': 'bus-large-2018', 'plans_page_hidden': False, }, { 'title': 'Carrier', 'price': 35000, 'privateRepos': 250, 'stripeId': 'bus-xlarge-30', 'audience': 'For extra large businesses', 'bus_features': True, 'deprecated': True, 'free_trial_days': 30, 'superseded_by': 'bus-xlarge-2018', 'plans_page_hidden': False, }, { 'title': 'Huge', 'price': 65000, 'privateRepos': 500, 'stripeId': 'bus-500-30', 'audience': 'For huge business', 'bus_features': True, 'deprecated': True, 'free_trial_days': 30, 'superseded_by': 'bus-500-2018', 'plans_page_hidden': False, }, { 'title': 'Huuge', 'price': 120000, 'privateRepos': 1000, 'stripeId': 'bus-1000-30', 'audience': 'For the SaaS savvy enterprise', 'bus_features': True, 'deprecated': True, 'free_trial_days': 30, 'superseded_by': 'bus-1000-2018', 'plans_page_hidden': False, }, # Active plans (as of Dec 2017) { 'title': 'Open Source', 'price': 0, 'privateRepos': 0, 'stripeId': 'free', 'audience': 'Committment to FOSS', 'bus_features': False, 'deprecated': False, 'free_trial_days': 30, 'superseded_by': None, 'plans_page_hidden': False, }, { 'title': 'Developer', 'price': 1500, 'privateRepos': 5, 'stripeId': 'personal-2018', 'audience': 'Individuals', 'bus_features': False, 'deprecated': False, 'free_trial_days': 30, 'superseded_by': None, 'plans_page_hidden': False, }, { 'title': 'Micro', 'price': 3000, 'privateRepos': 10, 'stripeId': 'bus-micro-2018', 'audience': 'For startups', 'bus_features': True, 'deprecated': False, 'free_trial_days': 30, 'superseded_by': None, 'plans_page_hidden': False, }, { 'title': 'Small', 'price': 6000, 'privateRepos': 20, 'stripeId': 'bus-small-2018', 'audience': 'For small businesses', 'bus_features': True, 'deprecated': False, 'free_trial_days': 30, 'superseded_by': None, 'plans_page_hidden': False, }, { 'title': 'Medium', 'price': 12500, 'privateRepos': 50, 'stripeId': 'bus-medium-2018', 'audience': 'For normal businesses', 'bus_features': True, 'deprecated': False, 'free_trial_days': 30, 'superseded_by': None, 'plans_page_hidden': False, }, { 'title': 'Large', 'price': 25000, 'privateRepos': 125, 'stripeId': 'bus-large-2018', 'audience': 'For large businesses', 'bus_features': True, 'deprecated': False, 'free_trial_days': 30, 'superseded_by': None, 'plans_page_hidden': False, }, { 'title': 'Extra Large', 'price': 45000, 'privateRepos': 250, 'stripeId': 'bus-xlarge-2018', 'audience': 'For extra large businesses', 'bus_features': True, 'deprecated': False, 'free_trial_days': 30, 'superseded_by': None, 'plans_page_hidden': False, }, { 'title': 'XXL', 'price': 85000, 'privateRepos': 500, 'stripeId': 'bus-500-2018', 'audience': 'For huge business', 'bus_features': True, 'deprecated': False, 'free_trial_days': 30, 'superseded_by': None, 'plans_page_hidden': False, }, { 'title': 'XXXL', 'price': 160000, 'privateRepos': 1000, 'stripeId': 'bus-1000-2018', 'audience': 'For the SaaS savvy enterprise', 'bus_features': True, 'deprecated': False, 'free_trial_days': 30, 'superseded_by': None, 'plans_page_hidden': False, }, ] 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)