mirror of
https://github.com/quay/quay.git
synced 2025-04-18 10:44:06 +03:00
645 lines
18 KiB
Python
645 lines
18 KiB
Python
import random
|
|
import string
|
|
import sys
|
|
from calendar import timegm
|
|
from datetime import datetime, timedelta
|
|
from typing import Any, Dict
|
|
|
|
import stripe
|
|
|
|
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,
|
|
"rh_sku": "FakeSKU",
|
|
"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",
|
|
"rh_sku": "MW00584MO",
|
|
"sku_billing": False,
|
|
"audience": "Individuals",
|
|
"bus_features": False,
|
|
"deprecated": False,
|
|
"free_trial_days": 30,
|
|
"superseded_by": None,
|
|
"plans_page_hidden": False,
|
|
},
|
|
{
|
|
"title": "Micro",
|
|
"price": 3000,
|
|
"privateRepos": 10,
|
|
"rh_sku": "MW00585MO",
|
|
"sku_billing": False,
|
|
"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,
|
|
"rh_sku": "MW00586MO",
|
|
"sku_billing": False,
|
|
"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,
|
|
"rh_sku": "MW00587MO",
|
|
"sku_billing": False,
|
|
"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,
|
|
"rh_sku": "MW00588MO",
|
|
"sku_billing": False,
|
|
"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,
|
|
"rh_sku": "MW00589MO",
|
|
"billing_enabled": False,
|
|
"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,
|
|
"rh_sku": "MW00590MO",
|
|
"billing_enabled": False,
|
|
"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,
|
|
"rh_sku": "MW00591MO",
|
|
"sku_billing": False,
|
|
"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,
|
|
},
|
|
{
|
|
"title": "XXXXL",
|
|
"price": 310000,
|
|
"privateRepos": 2000,
|
|
"rh_sku": "MW00592MO",
|
|
"sku_billing": False,
|
|
"stripeId": "bus-2000-2018",
|
|
"audience": "For the SaaS savvy big enterprise",
|
|
"bus_features": True,
|
|
"deprecated": False,
|
|
"free_trial_days": 30,
|
|
"superseded_by": None,
|
|
"plans_page_hidden": False,
|
|
},
|
|
{
|
|
"title": "XXXXXL",
|
|
"price": 2170000,
|
|
"privateRepos": 15000,
|
|
"stripeId": "price_1LRztA2OoNF1TIf0SvSrz106",
|
|
"audience": "For the SaaS savvy very big enterprise",
|
|
"bus_features": True,
|
|
"deprecated": False,
|
|
"free_trial_days": 30,
|
|
"superseded_by": None,
|
|
"plans_page_hidden": False,
|
|
},
|
|
{
|
|
"title": "premium",
|
|
"privateRepos": 100,
|
|
"stripeId": "not_a_stripe_plan",
|
|
"rh_sku": "MW02701",
|
|
"sku_billing": True,
|
|
"plans_page_hidden": True,
|
|
},
|
|
{
|
|
"title": "selfsupport",
|
|
"privateRepos": sys.maxsize,
|
|
"stripeId": "not_a_stripe_plan",
|
|
"rh_sku": "MW02702",
|
|
"sku_billing": True,
|
|
"plans_page_hidden": True,
|
|
},
|
|
{
|
|
"title": "freetier",
|
|
"privateRepos": 0,
|
|
"stripeId": "not_a_stripe_plan",
|
|
"rh_sku": "MW04192",
|
|
"sku_billing": False,
|
|
"plans_page_hidden": True,
|
|
},
|
|
]
|
|
|
|
RH_SKUS = [plan["rh_sku"] for plan in PLANS if plan.get("rh_sku") is not None]
|
|
|
|
RECONCILER_SKUS = [
|
|
plan["rh_sku"]
|
|
for plan in PLANS
|
|
if plan.get("rh_sku") is not None and not plan.get("sku_billing")
|
|
]
|
|
|
|
|
|
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
|
|
|
|
|
|
def get_plan_using_rh_sku(sku):
|
|
"""
|
|
Returns the plan with given sku or None if none.
|
|
"""
|
|
if sku is None:
|
|
return None
|
|
for plan in PLANS:
|
|
if plan.get("rh_sku") == sku:
|
|
return plan
|
|
return None
|
|
|
|
|
|
class FakeStripe(object):
|
|
ACTIVE_CUSTOMERS: Dict[str, Any] = {}
|
|
|
|
class error(object):
|
|
class InvalidRequestException(Exception):
|
|
pass
|
|
|
|
class APIConnectionError(Exception):
|
|
pass
|
|
|
|
class FakeSubscription(AttrDict):
|
|
@classmethod
|
|
def build(cls, data, customer):
|
|
data = AttrDict.deep_copy(data)
|
|
data["customer"] = customer
|
|
data["id"] = "sub_" + "".join(random.choices(string.ascii_lowercase, k=14))
|
|
return cls(data)
|
|
|
|
@classmethod
|
|
def delete(cls, sub_id):
|
|
for _, cus_obj in FakeStripe.ACTIVE_CUSTOMERS.items():
|
|
if cus_obj._subscription and cus_obj._subscription.id == sub_id:
|
|
cus_obj._subscription = None
|
|
|
|
def default_payment_method(self, payment_method):
|
|
return "pm_somestripepaymentmethodid"
|
|
|
|
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],
|
|
}
|
|
)
|
|
|
|
@property
|
|
def id(self):
|
|
return self.get("id", None)
|
|
|
|
@property
|
|
def subscription(self):
|
|
return FakeStripe.ACTIVE_CUSTOMERS[self.id]._subscription
|
|
|
|
@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 refresh(self):
|
|
return self
|
|
|
|
def save(self):
|
|
if self.get("new_card", None) is not None:
|
|
raise stripe.error.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 = FakeStripe.FakeSubscription.build(
|
|
self.FAKE_SUBSCRIPTION, self
|
|
)
|
|
self._subscription.plan.id = self.get("new_plan")
|
|
|
|
@classmethod
|
|
def retrieve(cls, stripe_customer_id):
|
|
if stripe_customer_id in FakeStripe.ACTIVE_CUSTOMERS:
|
|
FakeStripe.ACTIVE_CUSTOMERS[stripe_customer_id].pop("new_card", None)
|
|
FakeStripe.ACTIVE_CUSTOMERS[stripe_customer_id].pop("new_plan", None)
|
|
|
|
return FakeStripe.ACTIVE_CUSTOMERS[stripe_customer_id]
|
|
else:
|
|
new_customer = cls(
|
|
{
|
|
"default_card": "card123",
|
|
"cards": AttrDict.deep_copy(cls.FAKE_CARD_LIST),
|
|
"id": stripe_customer_id,
|
|
}
|
|
)
|
|
|
|
FakeStripe.ACTIVE_CUSTOMERS[stripe_customer_id] = new_customer
|
|
|
|
new_customer._subscription = FakeStripe.FakeSubscription.build(
|
|
cls.FAKE_SUBSCRIPTION, new_customer
|
|
)
|
|
|
|
return new_customer
|
|
|
|
@classmethod
|
|
def create(cls, **kwargs):
|
|
cus_id = "cus_" + "".join(random.choices(string.ascii_lowercase, k=14))
|
|
new_customer = cls(
|
|
{
|
|
"default_card": "card123",
|
|
"cards": AttrDict.deep_copy(cls.FAKE_CARD_LIST),
|
|
"id": cus_id,
|
|
}
|
|
)
|
|
new_customer._subscription = FakeStripe.FakeSubscription.build(
|
|
cls.FAKE_SUBSCRIPTION, new_customer
|
|
)
|
|
FakeStripe.ACTIVE_CUSTOMERS[stripe_customer_id] = new_customer
|
|
return new_customer
|
|
|
|
@classmethod
|
|
def modify(cls, cus_id, **kwargs):
|
|
customer = FakeStripe.ACTIVE_CUSTOMERS.get(cus_id)
|
|
if not customer:
|
|
# For testing, assume customer exists
|
|
customer = cls(
|
|
{
|
|
"default_card": "card123",
|
|
"cards": AttrDict.deep_copy(cls.FAKE_CARD_LIST),
|
|
"id": cus_id,
|
|
}
|
|
)
|
|
customer._subscription = FakeStripe.FakeSubscription.build(
|
|
cls.FAKE_SUBSCRIPTION, new_customer
|
|
)
|
|
FakeStripe.ACTIVE_CUSTOMERS[cus_id] = customer
|
|
|
|
if kwargs.get("plan"):
|
|
if customer._subscription is None:
|
|
customer._subscription = FakeStripe.FakeSubscription.build(
|
|
cls.FAKE_SUBSCRIPTION, customer
|
|
)
|
|
customer._subscription.plan.id = kwargs.get("plan")
|
|
|
|
return customer
|
|
|
|
class Invoice(AttrDict):
|
|
@staticmethod
|
|
def list(customer, count):
|
|
return AttrDict(
|
|
{
|
|
"data": [],
|
|
}
|
|
)
|
|
|
|
class PaymentMethod(AttrDict):
|
|
FAKE_PAYMENT_METHOD = AttrDict(
|
|
{
|
|
"id": "card123",
|
|
"type": "Visa",
|
|
"card": AttrDict(
|
|
{
|
|
"last4": "4242",
|
|
"exp_month": 5,
|
|
"exp_year": 2016,
|
|
}
|
|
),
|
|
"billing_details": AttrDict({"name": "Joe User"}),
|
|
}
|
|
)
|
|
|
|
@classmethod
|
|
def retrieve(cls, payment_method_id):
|
|
return cls.FAKE_PAYMENT_METHOD
|
|
|
|
Subscription = FakeSubscription
|
|
|
|
|
|
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)
|