mirror of
https://github.com/quay/quay.git
synced 2025-07-30 07:43:13 +03:00
billing: update Stripe checkout to support 3DS (PROJQUAY-5129) (#1818)
Update Stripe checkout in order to support auth requirements from banks.
This commit is contained in:
committed by
GitHub
parent
fa50c70ed0
commit
89725309be
@ -367,6 +367,9 @@ class FakeStripe(object):
|
|||||||
if cus_obj._subscription and cus_obj._subscription.id == sub_id:
|
if cus_obj._subscription and cus_obj._subscription.id == sub_id:
|
||||||
cus_obj._subscription = None
|
cus_obj._subscription = None
|
||||||
|
|
||||||
|
def default_payment_method(self, payment_method):
|
||||||
|
return "pm_somestripepaymentmethodid"
|
||||||
|
|
||||||
class Customer(AttrDict):
|
class Customer(AttrDict):
|
||||||
FAKE_PLAN = AttrDict(
|
FAKE_PLAN = AttrDict(
|
||||||
{
|
{
|
||||||
@ -427,6 +430,9 @@ class FakeStripe(object):
|
|||||||
def plan(self, plan_name):
|
def plan(self, plan_name):
|
||||||
self["new_plan"] = plan_name
|
self["new_plan"] = plan_name
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
return self
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
if self.get("new_card", None) is not None:
|
if self.get("new_card", None) is not None:
|
||||||
raise stripe.error.CardError(
|
raise stripe.error.CardError(
|
||||||
@ -514,6 +520,26 @@ class FakeStripe(object):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
Subscription = FakeSubscription
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,13 +20,21 @@ from endpoints.api import (
|
|||||||
require_scope,
|
require_scope,
|
||||||
abort,
|
abort,
|
||||||
)
|
)
|
||||||
from endpoints.exception import Unauthorized, NotFound
|
|
||||||
from endpoints.api.subscribe import subscribe, subscription_view
|
from endpoints.exception import Unauthorized, NotFound, InvalidRequest
|
||||||
|
from endpoints.api.subscribe import (
|
||||||
|
get_price,
|
||||||
|
change_subscription,
|
||||||
|
subscription_view,
|
||||||
|
connection_response,
|
||||||
|
check_repository_usage,
|
||||||
|
)
|
||||||
from auth.permissions import AdministerOrganizationPermission
|
from auth.permissions import AdministerOrganizationPermission
|
||||||
from auth.auth_context import get_authenticated_user
|
from auth.auth_context import get_authenticated_user
|
||||||
from auth import scopes
|
from auth import scopes
|
||||||
from data import model
|
from data import model
|
||||||
from data.billing import PLANS, get_plan
|
from data.billing import PLANS, get_plan
|
||||||
|
from util.request import get_request_ip
|
||||||
|
|
||||||
import features
|
import features
|
||||||
import uuid
|
import uuid
|
||||||
@ -85,18 +93,15 @@ def get_card(user):
|
|||||||
except stripe.error.APIConnectionError as e:
|
except stripe.error.APIConnectionError as e:
|
||||||
abort(503, message="Cannot contact Stripe")
|
abort(503, message="Cannot contact Stripe")
|
||||||
|
|
||||||
if cus and cus.default_card:
|
if cus and cus.subscription:
|
||||||
# Find the default card.
|
# Find the default card.
|
||||||
default_card = None
|
payment_method = billing.PaymentMethod.retrieve(cus.subscription.default_payment_method)
|
||||||
for card in cus.cards.data:
|
|
||||||
if card.id == cus.default_card:
|
|
||||||
default_card = card
|
|
||||||
break
|
|
||||||
|
|
||||||
if default_card:
|
if payment_method.card:
|
||||||
|
default_card = payment_method.card
|
||||||
card_info = {
|
card_info = {
|
||||||
"owner": default_card.name,
|
"owner": payment_method.billing_details.name,
|
||||||
"type": default_card.type,
|
"type": payment_method.type,
|
||||||
"last4": default_card.last4,
|
"last4": default_card.last4,
|
||||||
"exp_month": default_card.exp_month,
|
"exp_month": default_card.exp_month,
|
||||||
"exp_year": default_card.exp_year,
|
"exp_year": default_card.exp_year,
|
||||||
@ -105,27 +110,6 @@ def get_card(user):
|
|||||||
return {"card": card_info}
|
return {"card": card_info}
|
||||||
|
|
||||||
|
|
||||||
def set_card(user, token):
|
|
||||||
if user.stripe_id:
|
|
||||||
try:
|
|
||||||
cus = billing.Customer.retrieve(user.stripe_id)
|
|
||||||
except stripe.error.APIConnectionError as e:
|
|
||||||
abort(503, message="Cannot contact Stripe")
|
|
||||||
|
|
||||||
if cus:
|
|
||||||
try:
|
|
||||||
cus.card = token
|
|
||||||
cus.save()
|
|
||||||
except stripe.error.CardError as exc:
|
|
||||||
return carderror_response(exc)
|
|
||||||
except stripe.error.InvalidRequestError as exc:
|
|
||||||
return carderror_response(exc)
|
|
||||||
except stripe.error.APIConnectionError as e:
|
|
||||||
return carderror_response(e)
|
|
||||||
|
|
||||||
return get_card(user)
|
|
||||||
|
|
||||||
|
|
||||||
def get_invoices(customer_id):
|
def get_invoices(customer_id):
|
||||||
def invoice_view(i):
|
def invoice_view(i):
|
||||||
return {
|
return {
|
||||||
@ -218,13 +202,15 @@ class UserCard(ApiResource):
|
|||||||
"id": "UserCard",
|
"id": "UserCard",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Description of a user card",
|
"description": "Description of a user card",
|
||||||
"required": [
|
"required": ["success_url", "cancel_url"],
|
||||||
"token",
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"token": {
|
"success_url": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Stripe token that is generated by stripe checkout.js",
|
"description": "Redirect url after successful checkout",
|
||||||
|
},
|
||||||
|
"cancel_url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Redirect url after cancelled checkout",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -247,10 +233,46 @@ class UserCard(ApiResource):
|
|||||||
Update the user's credit card.
|
Update the user's credit card.
|
||||||
"""
|
"""
|
||||||
user = get_authenticated_user()
|
user = get_authenticated_user()
|
||||||
token = request.get_json()["token"]
|
assert user.stripe_id
|
||||||
response = set_card(user, token)
|
|
||||||
log_action("account_change_cc", user.username)
|
request_data = request.get_json()
|
||||||
return response
|
success_url = request_data["success_url"]
|
||||||
|
cancel_url = request_data["cancel_url"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
cus = billing.Customer.retrieve(user.stripe_id)
|
||||||
|
except stripe.error.APIConnectionError as e:
|
||||||
|
abort(503, message="Cannot contact Stripe")
|
||||||
|
|
||||||
|
if not cus:
|
||||||
|
raise InvalidRequest("Invalid Stripe customer")
|
||||||
|
|
||||||
|
if not cus.subscription:
|
||||||
|
raise InvalidRequest("Invalid Stripe subscription")
|
||||||
|
|
||||||
|
try:
|
||||||
|
checkout_session = stripe.checkout.Session.create(
|
||||||
|
payment_method_types=["card"],
|
||||||
|
mode="setup",
|
||||||
|
customer=user.stripe_id,
|
||||||
|
setup_intent_data={
|
||||||
|
"metadata": {
|
||||||
|
"kind": "account_change_cc",
|
||||||
|
"namespace": user.username,
|
||||||
|
"performer": user.username,
|
||||||
|
"ip": get_request_ip(),
|
||||||
|
"subscription_id": cus.subscription.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success_url=success_url,
|
||||||
|
cancel_url=cancel_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
return checkout_session
|
||||||
|
except stripe.error.APIConnectionError as se:
|
||||||
|
abort(503, message="Cannot contact Stripe: %s" % se)
|
||||||
|
except Exception as e:
|
||||||
|
abort(500, message=str(e))
|
||||||
|
|
||||||
|
|
||||||
@resource("/v1/organization/<orgname>/card")
|
@resource("/v1/organization/<orgname>/card")
|
||||||
@ -267,14 +289,16 @@ class OrganizationCard(ApiResource):
|
|||||||
"OrgCard": {
|
"OrgCard": {
|
||||||
"id": "OrgCard",
|
"id": "OrgCard",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Description of a user card",
|
"description": "Description of an Organization card",
|
||||||
"required": [
|
"required": ["success_url", "cancel_url"],
|
||||||
"token",
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"token": {
|
"success_url": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Stripe token that is generated by stripe checkout.js",
|
"description": "Redirect url after successful checkout",
|
||||||
|
},
|
||||||
|
"cancel_url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Redirect url after cancelled checkout",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -302,10 +326,46 @@ class OrganizationCard(ApiResource):
|
|||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
organization = model.organization.get_organization(orgname)
|
organization = model.organization.get_organization(orgname)
|
||||||
token = request.get_json()["token"]
|
assert organization.stripe_id
|
||||||
response = set_card(organization, token)
|
|
||||||
log_action("account_change_cc", orgname)
|
request_data = request.get_json()
|
||||||
return response
|
success_url = request_data["success_url"]
|
||||||
|
cancel_url = request_data["cancel_url"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
cus = billing.Customer.retrieve(organization.stripe_id)
|
||||||
|
except stripe.error.APIConnectionError as e:
|
||||||
|
abort(503, message="Cannot contact Stripe")
|
||||||
|
|
||||||
|
if not cus:
|
||||||
|
raise InvalidRequest("Invalid Stripe customer")
|
||||||
|
|
||||||
|
if not cus.subscription:
|
||||||
|
raise InvalidRequest("Invalid Stripe subscription")
|
||||||
|
|
||||||
|
try:
|
||||||
|
checkout_session = stripe.checkout.Session.create(
|
||||||
|
payment_method_types=["card"],
|
||||||
|
mode="setup",
|
||||||
|
customer=organization.stripe_id,
|
||||||
|
setup_intent_data={
|
||||||
|
"metadata": {
|
||||||
|
"kind": "account_change_cc",
|
||||||
|
"namespace": organization.username,
|
||||||
|
"performer": get_authenticated_user().username,
|
||||||
|
"ip": get_request_ip(),
|
||||||
|
"subscription_id": cus.subscription.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
success_url=success_url,
|
||||||
|
cancel_url=cancel_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
return checkout_session
|
||||||
|
except stripe.error.APIConnectionError as se:
|
||||||
|
abort(503, message="Cannot contact Stripe: %s" % se)
|
||||||
|
except Exception as e:
|
||||||
|
abort(500, message=str(e))
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
@ -323,34 +383,101 @@ class UserPlan(ApiResource):
|
|||||||
"id": "UserSubscription",
|
"id": "UserSubscription",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Description of a user card",
|
"description": "Description of a user card",
|
||||||
"required": [
|
"required": ["plan"],
|
||||||
"plan",
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"token": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Stripe token that is generated by stripe checkout.js",
|
|
||||||
},
|
|
||||||
"plan": {
|
"plan": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Plan name to which the user wants to subscribe",
|
"description": "Plan name to which the user wants to subscribe",
|
||||||
},
|
},
|
||||||
|
"success_url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Redirect url after successful checkout",
|
||||||
|
},
|
||||||
|
"cancel_url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Redirect url after cancelled checkout",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@require_user_admin()
|
||||||
|
@nickname("createUserSubscription")
|
||||||
|
@validate_json_request("UserSubscription")
|
||||||
|
def post(self):
|
||||||
|
"""
|
||||||
|
Create the user's subscription. Returns a Stripe checkout session.
|
||||||
|
"""
|
||||||
|
request_data = request.get_json()
|
||||||
|
plan = request_data["plan"]
|
||||||
|
success_url = request_data.get("success_url")
|
||||||
|
cancel_url = request_data.get("cancel_url")
|
||||||
|
|
||||||
|
if not success_url or not cancel_url:
|
||||||
|
raise InvalidRequest()
|
||||||
|
|
||||||
|
user = get_authenticated_user()
|
||||||
|
if not user.stripe_id:
|
||||||
|
try:
|
||||||
|
cus = billing.Customer.create(
|
||||||
|
email=user.email,
|
||||||
|
)
|
||||||
|
user.stripe_id = cus.id
|
||||||
|
user.save()
|
||||||
|
except stripe.error.APIConnectionError as e:
|
||||||
|
return connection_response(e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
price = get_price(plan, False)
|
||||||
|
if not price:
|
||||||
|
abort(404, message="Plan not found")
|
||||||
|
|
||||||
|
checkout_session = stripe.checkout.Session.create(
|
||||||
|
line_items=[
|
||||||
|
{
|
||||||
|
"price": price["stripeId"],
|
||||||
|
"quantity": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
customer=user.stripe_id,
|
||||||
|
subscription_data={
|
||||||
|
"metadata": {
|
||||||
|
"kind": "account_change_plan",
|
||||||
|
"namespace": user.username,
|
||||||
|
"performer": user.username,
|
||||||
|
"ip": get_request_ip(),
|
||||||
|
"plan": price["stripeId"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mode="subscription",
|
||||||
|
success_url=success_url,
|
||||||
|
cancel_url=cancel_url,
|
||||||
|
)
|
||||||
|
return checkout_session
|
||||||
|
except stripe.error.APIConnectionError as e:
|
||||||
|
abort(503, message="Cannot contact Stripe")
|
||||||
|
except Exception as e:
|
||||||
|
abort(500, message=str(e))
|
||||||
|
|
||||||
@require_user_admin()
|
@require_user_admin()
|
||||||
@nickname("updateUserSubscription")
|
@nickname("updateUserSubscription")
|
||||||
@validate_json_request("UserSubscription")
|
@validate_json_request("UserSubscription")
|
||||||
def put(self):
|
def put(self):
|
||||||
"""
|
"""
|
||||||
Create or update the user's subscription.
|
Update the user's existing subscription.
|
||||||
"""
|
"""
|
||||||
request_data = request.get_json()
|
request_data = request.get_json()
|
||||||
plan = request_data["plan"]
|
plan = request_data["plan"]
|
||||||
token = request_data["token"] if "token" in request_data else None
|
|
||||||
user = get_authenticated_user()
|
user = get_authenticated_user()
|
||||||
return subscribe(user, plan, token, False) # Business features not required
|
if not user.stripe_id:
|
||||||
|
raise InvalidRequest()
|
||||||
|
|
||||||
|
price = get_price(plan, False)
|
||||||
|
if not price:
|
||||||
|
abort(404, message="Plan not found")
|
||||||
|
|
||||||
|
return change_subscription(user, price)
|
||||||
|
|
||||||
@require_user_admin()
|
@require_user_admin()
|
||||||
@nickname("getUserSubscription")
|
@nickname("getUserSubscription")
|
||||||
@ -394,36 +521,107 @@ class OrganizationPlan(ApiResource):
|
|||||||
"id": "OrgSubscription",
|
"id": "OrgSubscription",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Description of a user card",
|
"description": "Description of a user card",
|
||||||
"required": [
|
"required": ["plan"],
|
||||||
"plan",
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"token": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Stripe token that is generated by stripe checkout.js",
|
|
||||||
},
|
|
||||||
"plan": {
|
"plan": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Plan name to which the user wants to subscribe",
|
"description": "Plan name to which the user wants to subscribe",
|
||||||
},
|
},
|
||||||
|
"success_url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Redirect url after successful checkout",
|
||||||
|
},
|
||||||
|
"cancel_url": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Redirect url after cancelled checkout",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@require_scope(scopes.ORG_ADMIN)
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
@nickname("updateOrgSubscription")
|
@nickname("createOrgSubscription")
|
||||||
@validate_json_request("OrgSubscription")
|
@validate_json_request("OrgSubscription")
|
||||||
def put(self, orgname):
|
def post(self, orgname):
|
||||||
"""
|
"""
|
||||||
Create or update the org's subscription.
|
Create the org's subscription. Returns a Stripe checkout session.
|
||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
request_data = request.get_json()
|
request_data = request.get_json()
|
||||||
plan = request_data["plan"]
|
plan = request_data["plan"]
|
||||||
token = request_data["token"] if "token" in request_data else None
|
success_url = request_data.get("success_url")
|
||||||
|
cancel_url = request_data.get("cancel_url")
|
||||||
|
|
||||||
|
if not success_url or not cancel_url:
|
||||||
|
raise InvalidRequest()
|
||||||
|
|
||||||
organization = model.organization.get_organization(orgname)
|
organization = model.organization.get_organization(orgname)
|
||||||
return subscribe(organization, plan, token, True) # Business plan required
|
if not organization.stripe_id:
|
||||||
|
try:
|
||||||
|
cus = billing.Customer.create(
|
||||||
|
email=organization.email,
|
||||||
|
)
|
||||||
|
organization.stripe_id = cus.id
|
||||||
|
organization.save()
|
||||||
|
except stripe.error.APIConnectionError as e:
|
||||||
|
return connection_response(e)
|
||||||
|
|
||||||
|
try:
|
||||||
|
price = get_price(plan, True)
|
||||||
|
if not price:
|
||||||
|
abort(404, message="Plan not found")
|
||||||
|
|
||||||
|
checkout_session = stripe.checkout.Session.create(
|
||||||
|
line_items=[
|
||||||
|
{
|
||||||
|
"price": price["stripeId"],
|
||||||
|
"quantity": 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
customer=organization.stripe_id,
|
||||||
|
subscription_data={
|
||||||
|
"metadata": {
|
||||||
|
"kind": "account_change_plan",
|
||||||
|
"namespace": organization.username,
|
||||||
|
"performer": get_authenticated_user().username,
|
||||||
|
"ip": get_request_ip(),
|
||||||
|
"plan": price["stripeId"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mode="subscription",
|
||||||
|
success_url=success_url,
|
||||||
|
cancel_url=cancel_url,
|
||||||
|
)
|
||||||
|
return checkout_session
|
||||||
|
except stripe.error.APIConnectionError as e:
|
||||||
|
abort(503, message="Cannot contact Stripe")
|
||||||
|
except Exception as e:
|
||||||
|
abort(500, message=str(e))
|
||||||
|
|
||||||
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
|
@nickname("updateOrgSubscription")
|
||||||
|
@validate_json_request("OrgSubscription")
|
||||||
|
def put(self, orgname):
|
||||||
|
"""
|
||||||
|
Update the org's subscription.
|
||||||
|
"""
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
request_data = request.get_json()
|
||||||
|
plan = request_data["plan"]
|
||||||
|
|
||||||
|
organization = model.organization.get_organization(orgname)
|
||||||
|
if not organization.stripe_id:
|
||||||
|
raise InvalidRequest()
|
||||||
|
|
||||||
|
price = get_price(plan, True)
|
||||||
|
if not price:
|
||||||
|
abort(404, message="Plan not found")
|
||||||
|
|
||||||
|
return change_subscription(organization, price)
|
||||||
|
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
|
@ -52,7 +52,8 @@ def subscription_view(stripe_subscription, used_repos):
|
|||||||
return view
|
return view
|
||||||
|
|
||||||
|
|
||||||
def subscribe(user, plan, token, require_business_plan):
|
def get_price(plan, require_business_plan):
|
||||||
|
"""Billing Price (previously stripe Plan) from id."""
|
||||||
if not features.BILLING:
|
if not features.BILLING:
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -69,6 +70,10 @@ def subscribe(user, plan, token, require_business_plan):
|
|||||||
logger.warning("Business attempting to subscribe to personal plan: %s", user.username)
|
logger.warning("Business attempting to subscribe to personal plan: %s", user.username)
|
||||||
raise request_error(message="No matching plan found")
|
raise request_error(message="No matching plan found")
|
||||||
|
|
||||||
|
return plan_found
|
||||||
|
|
||||||
|
|
||||||
|
def change_subscription(user, plan):
|
||||||
private_repos = model.get_private_repo_count(user.username)
|
private_repos = model.get_private_repo_count(user.username)
|
||||||
|
|
||||||
# This is the default response
|
# This is the default response
|
||||||
@ -78,41 +83,12 @@ def subscribe(user, plan, token, require_business_plan):
|
|||||||
}
|
}
|
||||||
status_code = 200
|
status_code = 200
|
||||||
|
|
||||||
if not user.stripe_id:
|
|
||||||
# Check if a non-paying user is trying to subscribe to a free plan
|
|
||||||
if not plan_found["price"] == 0:
|
|
||||||
# They want a real paying plan, create the customer and plan
|
|
||||||
# simultaneously
|
|
||||||
card = token
|
|
||||||
|
|
||||||
try:
|
|
||||||
cus = billing.Customer.create(
|
|
||||||
email=user.email,
|
|
||||||
plan=plan,
|
|
||||||
card=card,
|
|
||||||
payment_behavior="default_incomplete",
|
|
||||||
# API versions prior to 2019-03-14 defaults to 'error_if_incomplete'
|
|
||||||
)
|
|
||||||
user.stripe_id = cus.id
|
|
||||||
user.save()
|
|
||||||
check_repository_usage(user, plan_found)
|
|
||||||
log_action("account_change_plan", user.username, {"plan": plan})
|
|
||||||
except stripe.error.CardError as e:
|
|
||||||
return carderror_response(e)
|
|
||||||
except stripe.error.APIConnectionError as e:
|
|
||||||
return connection_response(e)
|
|
||||||
|
|
||||||
response_json = subscription_view(cus.subscription, private_repos)
|
|
||||||
status_code = 201
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Change the plan
|
|
||||||
try:
|
try:
|
||||||
cus = billing.Customer.retrieve(user.stripe_id)
|
cus = billing.Customer.retrieve(user.stripe_id)
|
||||||
except stripe.error.APIConnectionError as e:
|
except stripe.error.APIConnectionError as e:
|
||||||
return connection_response(e)
|
return connection_response(e)
|
||||||
|
|
||||||
if plan_found["price"] == 0:
|
if plan["price"] == 0:
|
||||||
if cus.subscription is not None:
|
if cus.subscription is not None:
|
||||||
# We only have to cancel the subscription if they actually have one
|
# We only have to cancel the subscription if they actually have one
|
||||||
try:
|
try:
|
||||||
@ -120,14 +96,12 @@ def subscribe(user, plan, token, require_business_plan):
|
|||||||
except stripe.error.APIConnectionError as e:
|
except stripe.error.APIConnectionError as e:
|
||||||
return connection_response(e)
|
return connection_response(e)
|
||||||
|
|
||||||
check_repository_usage(user, plan_found)
|
check_repository_usage(user, plan)
|
||||||
log_action("account_change_plan", user.username, {"plan": plan})
|
log_action("account_change_plan", user.username, {"plan": plan["stripeId"]})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# User may have been a previous customer who is resubscribing
|
# User may have been a previous customer who is resubscribing
|
||||||
modify_cus_args = {"plan": plan, "payment_behavior": "default_incomplete"}
|
modify_cus_args = {"plan": plan["stripeId"], "payment_behavior": "default_incomplete"}
|
||||||
if token:
|
|
||||||
modify_cus_args["card"] = token
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
billing.Customer.modify(cus.id, **modify_cus_args)
|
billing.Customer.modify(cus.id, **modify_cus_args)
|
||||||
@ -136,8 +110,9 @@ def subscribe(user, plan, token, require_business_plan):
|
|||||||
except stripe.error.APIConnectionError as e:
|
except stripe.error.APIConnectionError as e:
|
||||||
return connection_response(e)
|
return connection_response(e)
|
||||||
|
|
||||||
|
cus = cus.refresh()
|
||||||
response_json = subscription_view(cus.subscription, private_repos)
|
response_json = subscription_view(cus.subscription, private_repos)
|
||||||
check_repository_usage(user, plan_found)
|
check_repository_usage(user, plan)
|
||||||
log_action("account_change_plan", user.username, {"plan": plan})
|
log_action("account_change_plan", user.username, {"plan": plan["stripeId"]})
|
||||||
|
|
||||||
return response_json, status_code
|
return response_json, status_code
|
||||||
|
@ -346,12 +346,27 @@ SECURITY_TESTS: List[
|
|||||||
(UserCard, "GET", None, None, "devtable", 200),
|
(UserCard, "GET", None, None, "devtable", 200),
|
||||||
(UserCard, "GET", None, None, "freshuser", 200),
|
(UserCard, "GET", None, None, "freshuser", 200),
|
||||||
(UserCard, "GET", None, None, "reader", 200),
|
(UserCard, "GET", None, None, "reader", 200),
|
||||||
(UserCard, "POST", None, {"token": "ORH4"}, None, 401),
|
(UserCard, "POST", None, {"success_url": "http://foo", "cancel_url": "http://bar"}, None, 401),
|
||||||
(UserPlan, "GET", None, None, None, 401),
|
(UserPlan, "GET", None, None, None, 401),
|
||||||
(UserPlan, "GET", None, None, "devtable", 200),
|
(UserPlan, "GET", None, None, "devtable", 200),
|
||||||
(UserPlan, "GET", None, None, "freshuser", 200),
|
(UserPlan, "GET", None, None, "freshuser", 200),
|
||||||
(UserPlan, "GET", None, None, "reader", 200),
|
(UserPlan, "GET", None, None, "reader", 200),
|
||||||
(UserPlan, "PUT", None, {"plan": "1QIK"}, None, 401),
|
(
|
||||||
|
UserPlan,
|
||||||
|
"POST",
|
||||||
|
None,
|
||||||
|
{"plan": "1QIK", "success_url": "http://foo", "cancel_url": "http://bar"},
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
UserPlan,
|
||||||
|
"PUT",
|
||||||
|
None,
|
||||||
|
{"plan": "1QIK", "success_url": "http://foo", "cancel_url": "http://bar"},
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
(UserLogs, "GET", None, None, None, 401),
|
(UserLogs, "GET", None, None, None, 401),
|
||||||
(UserLogs, "GET", None, None, "devtable", 200),
|
(UserLogs, "GET", None, None, "devtable", 200),
|
||||||
(UserLogs, "GET", None, None, "freshuser", 200),
|
(UserLogs, "GET", None, None, "freshuser", 200),
|
||||||
@ -3976,13 +3991,42 @@ SECURITY_TESTS: List[
|
|||||||
(OrganizationCard, "GET", {"orgname": "buynlarge"}, None, "devtable", 200),
|
(OrganizationCard, "GET", {"orgname": "buynlarge"}, None, "devtable", 200),
|
||||||
(OrganizationCard, "GET", {"orgname": "buynlarge"}, None, "freshuser", 403),
|
(OrganizationCard, "GET", {"orgname": "buynlarge"}, None, "freshuser", 403),
|
||||||
(OrganizationCard, "GET", {"orgname": "buynlarge"}, None, "reader", 403),
|
(OrganizationCard, "GET", {"orgname": "buynlarge"}, None, "reader", 403),
|
||||||
(OrganizationCard, "POST", {"orgname": "buynlarge"}, {"token": "4VFR"}, None, 401),
|
(
|
||||||
(OrganizationCard, "POST", {"orgname": "buynlarge"}, {"token": "4VFR"}, "freshuser", 403),
|
OrganizationCard,
|
||||||
(OrganizationCard, "POST", {"orgname": "buynlarge"}, {"token": "4VFR"}, "reader", 403),
|
"POST",
|
||||||
|
{"orgname": "buynlarge"},
|
||||||
|
{"success_url": "http://foo", "cancel_url": "http://bar"},
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationCard,
|
||||||
|
"POST",
|
||||||
|
{"orgname": "buynlarge"},
|
||||||
|
{"success_url": "http://foo", "cancel_url": "http://bar"},
|
||||||
|
"freshuser",
|
||||||
|
403,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
OrganizationCard,
|
||||||
|
"POST",
|
||||||
|
{"orgname": "buynlarge"},
|
||||||
|
{"success_url": "http://foo", "cancel_url": "http://bar"},
|
||||||
|
"reader",
|
||||||
|
403,
|
||||||
|
),
|
||||||
(OrganizationPlan, "GET", {"orgname": "buynlarge"}, None, None, 401),
|
(OrganizationPlan, "GET", {"orgname": "buynlarge"}, None, None, 401),
|
||||||
(OrganizationPlan, "GET", {"orgname": "buynlarge"}, None, "devtable", 200),
|
(OrganizationPlan, "GET", {"orgname": "buynlarge"}, None, "devtable", 200),
|
||||||
(OrganizationPlan, "GET", {"orgname": "buynlarge"}, None, "freshuser", 403),
|
(OrganizationPlan, "GET", {"orgname": "buynlarge"}, None, "freshuser", 403),
|
||||||
(OrganizationPlan, "GET", {"orgname": "buynlarge"}, None, "reader", 403),
|
(OrganizationPlan, "GET", {"orgname": "buynlarge"}, None, "reader", 403),
|
||||||
|
(
|
||||||
|
OrganizationPlan,
|
||||||
|
"POST",
|
||||||
|
{"orgname": "buynlarge"},
|
||||||
|
{"plan": "WWEI", "success_url": "http://foo", "cancel_url": "http://bar"},
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
(OrganizationPlan, "PUT", {"orgname": "buynlarge"}, {"plan": "WWEI"}, None, 401),
|
(OrganizationPlan, "PUT", {"orgname": "buynlarge"}, {"plan": "WWEI"}, None, 401),
|
||||||
(OrganizationPlan, "PUT", {"orgname": "buynlarge"}, {"plan": "WWEI"}, "freshuser", 403),
|
(OrganizationPlan, "PUT", {"orgname": "buynlarge"}, {"plan": "WWEI"}, "freshuser", 403),
|
||||||
(OrganizationPlan, "PUT", {"orgname": "buynlarge"}, {"plan": "WWEI"}, "reader", 403),
|
(OrganizationPlan, "PUT", {"orgname": "buynlarge"}, {"plan": "WWEI"}, "reader", 403),
|
||||||
|
@ -59,7 +59,7 @@ from endpoints.api import (
|
|||||||
page_support,
|
page_support,
|
||||||
)
|
)
|
||||||
from endpoints.exception import NotFound, InvalidToken, InvalidRequest, DownstreamIssue
|
from endpoints.exception import NotFound, InvalidToken, InvalidRequest, DownstreamIssue
|
||||||
from endpoints.api.subscribe import subscribe
|
from endpoints.api.subscribe import change_subscription, get_price
|
||||||
from endpoints.common import common_login
|
from endpoints.common import common_login
|
||||||
from endpoints.csrf import generate_csrf_token, OAUTH_CSRF_TOKEN_NAME
|
from endpoints.csrf import generate_csrf_token, OAUTH_CSRF_TOKEN_NAME
|
||||||
from endpoints.decorators import (
|
from endpoints.decorators import (
|
||||||
@ -734,7 +734,8 @@ class ConvertToOrganization(ApiResource):
|
|||||||
# Subscribe the organization to the new plan.
|
# Subscribe the organization to the new plan.
|
||||||
if features.BILLING:
|
if features.BILLING:
|
||||||
plan = convert_data.get("plan", "free")
|
plan = convert_data.get("plan", "free")
|
||||||
subscribe(user, plan, None, True) # Require business plans
|
price = get_price(plan, True)
|
||||||
|
change_subscription(user, price) # Require business plans
|
||||||
|
|
||||||
# Convert the user to an organization.
|
# Convert the user to an organization.
|
||||||
model.organization.convert_user_to_organization(user, admin_user)
|
model.organization.convert_user_to_organization(user, admin_user)
|
||||||
|
@ -113,7 +113,7 @@ def render_page_template(name, route_data=None, **kwargs):
|
|||||||
|
|
||||||
# Add Stripe checkout if billing is enabled.
|
# Add Stripe checkout if billing is enabled.
|
||||||
if features.BILLING:
|
if features.BILLING:
|
||||||
external_scripts.append("//checkout.stripe.com/checkout.js")
|
external_scripts.append("//js.stripe.com/v3/")
|
||||||
|
|
||||||
has_contact = len(app.config.get("CONTACT_INFO", [])) > 0
|
has_contact = len(app.config.get("CONTACT_INFO", [])) > 0
|
||||||
contact_href = None
|
contact_href = None
|
||||||
|
@ -4,6 +4,7 @@ from flask import request, make_response, Blueprint
|
|||||||
|
|
||||||
from app import billing as stripe, app
|
from app import billing as stripe, app
|
||||||
from data import model
|
from data import model
|
||||||
|
from data.logs_model import logs_model
|
||||||
from data.database import RepositoryState
|
from data.database import RepositoryState
|
||||||
from auth.decorators import process_auth
|
from auth.decorators import process_auth
|
||||||
from auth.permissions import ModifyRepositoryPermission
|
from auth.permissions import ModifyRepositoryPermission
|
||||||
@ -30,6 +31,18 @@ webhooks = Blueprint("webhooks", __name__)
|
|||||||
|
|
||||||
@webhooks.route("/stripe", methods=["POST"])
|
@webhooks.route("/stripe", methods=["POST"])
|
||||||
def stripe_webhook():
|
def stripe_webhook():
|
||||||
|
def _stripe_checkout_log_action(kind, namespace_name, performer_name, ip, metadata=None):
|
||||||
|
if not metadata:
|
||||||
|
metadata = {}
|
||||||
|
|
||||||
|
performer = data.model.users.get_user(performer_name)
|
||||||
|
logs_model.log_action(
|
||||||
|
kind,
|
||||||
|
namespace_name,
|
||||||
|
performer=performer,
|
||||||
|
ip=ip,
|
||||||
|
)
|
||||||
|
|
||||||
request_data = request.get_json()
|
request_data = request.get_json()
|
||||||
logger.debug("Stripe webhook call: %s", request_data)
|
logger.debug("Stripe webhook call: %s", request_data)
|
||||||
|
|
||||||
@ -83,6 +96,47 @@ def stripe_webhook():
|
|||||||
if namespace:
|
if namespace:
|
||||||
send_payment_failed(namespace.email, namespace.username)
|
send_payment_failed(namespace.email, namespace.username)
|
||||||
|
|
||||||
|
elif event_type == "checkout.session.completed":
|
||||||
|
mode = request_data["data"]["object"]["mode"]
|
||||||
|
|
||||||
|
if mode == "setup":
|
||||||
|
setup_intent = stripe.SetupIntent.retrieve(
|
||||||
|
request_data["data"]["object"]["setup_intent"]
|
||||||
|
)
|
||||||
|
setup_intent_metadata = setup_intent["metadata"]
|
||||||
|
|
||||||
|
payment_method = setup_intent["payment_method"]
|
||||||
|
customer = setup_intent["customer"]
|
||||||
|
subscription = setup_intent_metadata["subscription_id"]
|
||||||
|
|
||||||
|
stripe.Customer.modify(
|
||||||
|
customer,
|
||||||
|
invoice_settings={"default_payment_method": payment_method},
|
||||||
|
)
|
||||||
|
stripe.Subscription.modify(
|
||||||
|
subscription,
|
||||||
|
default_payment_method=payment_method,
|
||||||
|
)
|
||||||
|
|
||||||
|
_stripe_checkout_log_action(
|
||||||
|
setup_intent_metadata["kind"],
|
||||||
|
setup_intent_metadata["namespace"],
|
||||||
|
setup_intent_metadata["performer"],
|
||||||
|
setup_intent_metadata["ip"],
|
||||||
|
)
|
||||||
|
|
||||||
|
elif mode == "subscription":
|
||||||
|
sub = stripe.Subscription.retrieve(request_data["data"]["object"]["subscription"])
|
||||||
|
sub_metadata = sub["metadata"]
|
||||||
|
|
||||||
|
_stripe_checkout_log_action(
|
||||||
|
sub_metadata["kind"],
|
||||||
|
sub_metadata["namespace"],
|
||||||
|
sub_metadata["performer"],
|
||||||
|
sub_metadata["ip"],
|
||||||
|
metadata={"plan": sub_metadata["plan"]},
|
||||||
|
)
|
||||||
|
|
||||||
return make_response("Okay")
|
return make_response("Okay")
|
||||||
|
|
||||||
|
|
||||||
|
@ -2,9 +2,8 @@
|
|||||||
* Helper service for loading, changing and working with subscription plans.
|
* Helper service for loading, changing and working with subscription plans.
|
||||||
*/
|
*/
|
||||||
angular.module('quay')
|
angular.module('quay')
|
||||||
.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features', 'Config', '$location',
|
.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features', 'Config', '$location', '$timeout',
|
||||||
|
function(KeyService, UserService, CookieService, ApiService, Features, Config, $location, $timeout) {
|
||||||
function(KeyService, UserService, CookieService, ApiService, Features, Config, $location) {
|
|
||||||
var plans = null;
|
var plans = null;
|
||||||
var planDict = {};
|
var planDict = {};
|
||||||
var planService = {};
|
var planService = {};
|
||||||
@ -176,25 +175,45 @@ function(KeyService, UserService, CookieService, ApiService, Features, Config, $
|
|||||||
ApiService.getSubscription(orgname).then(success, failure);
|
ApiService.getSubscription(orgname).then(success, failure);
|
||||||
};
|
};
|
||||||
|
|
||||||
planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
|
planService.createSubscription = function($scope, orgname, planId, success, failure) {
|
||||||
|
if (!Features.BILLING) { return; }
|
||||||
|
|
||||||
|
var redirectURL = $scope.redirectUrl || window.location.toString();
|
||||||
|
var subscriptionDetails = {
|
||||||
|
plan: planId,
|
||||||
|
success_url: redirectURL,
|
||||||
|
cancel_url: redirectURL
|
||||||
|
};
|
||||||
|
|
||||||
|
ApiService.createSubscription(orgname, subscriptionDetails).then(
|
||||||
|
function(resp) {
|
||||||
|
$timeout(function() {
|
||||||
|
success(resp);
|
||||||
|
document.location = resp.url;
|
||||||
|
}, 250);
|
||||||
|
},
|
||||||
|
failure
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
planService.updateSubscription = function($scope, orgname, planId, success, failure) {
|
||||||
if (!Features.BILLING) { return; }
|
if (!Features.BILLING) { return; }
|
||||||
|
|
||||||
var subscriptionDetails = {
|
var subscriptionDetails = {
|
||||||
plan: planId
|
plan: planId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (opt_token) {
|
|
||||||
subscriptionDetails['token'] = opt_token.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApiService.updateSubscription(orgname, subscriptionDetails).then(function(resp) {
|
|
||||||
success(resp);
|
|
||||||
planService.getPlan(planId, function(plan) {
|
planService.getPlan(planId, function(plan) {
|
||||||
|
ApiService.updateSubscription(orgname, subscriptionDetails).then(
|
||||||
|
function(resp) {
|
||||||
|
success(resp);
|
||||||
for (var i = 0; i < listeners.length; ++i) {
|
for (var i = 0; i < listeners.length; ++i) {
|
||||||
listeners[i]['callback'](plan);
|
listeners[i]['callback'](plan);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
failure
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}, failure);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
planService.getCardInfo = function(orgname, callback) {
|
planService.getCardInfo = function(orgname, callback) {
|
||||||
@ -232,15 +251,25 @@ function(KeyService, UserService, CookieService, ApiService, Features, Config, $
|
|||||||
if (orgname && !planService.isOrgCompatible(plan)) { return; }
|
if (orgname && !planService.isOrgCompatible(plan)) { return; }
|
||||||
|
|
||||||
planService.getCardInfo(orgname, function(cardInfo) {
|
planService.getCardInfo(orgname, function(cardInfo) {
|
||||||
|
|
||||||
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4 || !opt_reuseCard)) {
|
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4 || !opt_reuseCard)) {
|
||||||
var title = cardInfo.last4 ? 'Subscribe' : 'Start Trial ({{amount}} plan)';
|
var redirectURL = $scope.redirectUrl || window.location.toString();
|
||||||
planService.showSubscribeDialog($scope, orgname, planId, callbacks, title, /* async */true);
|
var setCardDetails = {
|
||||||
|
success_url: redirectURL,
|
||||||
|
cancel_url: redirectURL
|
||||||
|
};
|
||||||
|
|
||||||
|
planService.createSubscription($scope, orgname, planId, callbacks['success'], function(resp) {
|
||||||
|
previousSubscribeFailure = true;
|
||||||
|
planService.handleCardError(resp);
|
||||||
|
callbacks['failure'](resp);
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
previousSubscribeFailure = false;
|
previousSubscribeFailure = false;
|
||||||
|
|
||||||
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
|
planService.updateSubscription($scope, orgname, planId, callbacks['success'], function(resp) {
|
||||||
previousSubscribeFailure = true;
|
previousSubscribeFailure = true;
|
||||||
planService.handleCardError(resp);
|
planService.handleCardError(resp);
|
||||||
callbacks['failure'](resp);
|
callbacks['failure'](resp);
|
||||||
@ -256,125 +285,25 @@ function(KeyService, UserService, CookieService, ApiService, Features, Config, $
|
|||||||
callbacks['opening']();
|
callbacks['opening']();
|
||||||
}
|
}
|
||||||
|
|
||||||
var submitted = false;
|
var redirectURL = $scope.redirectUrl || window.location.toString();
|
||||||
var submitToken = function(token) {
|
var setCardDetails = {
|
||||||
if (submitted) { return; }
|
success_url: redirectURL,
|
||||||
submitted = true;
|
cancel_url: redirectURL
|
||||||
$scope.$apply(function() {
|
|
||||||
if (callbacks['started']) {
|
|
||||||
callbacks['started']();
|
|
||||||
}
|
|
||||||
|
|
||||||
var cardInfo = {
|
|
||||||
'token': token.id
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ApiService.setCard(orgname, cardInfo).then(callbacks['success'], function(resp) {
|
ApiService.setCard(orgname, setCardDetails).then(
|
||||||
|
function(resp) {
|
||||||
|
$timeout(function() {
|
||||||
|
callbacks['success'](resp)
|
||||||
|
document.location = resp.url;
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
},
|
||||||
|
function(resp) {
|
||||||
planService.handleCardError(resp);
|
planService.handleCardError(resp);
|
||||||
callbacks['failure'](resp);
|
callbacks['failure'](resp);
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var email = planService.getEmail(orgname);
|
|
||||||
StripeCheckout.open({
|
|
||||||
key: KeyService.stripePublishableKey,
|
|
||||||
email: email,
|
|
||||||
currency: 'usd',
|
|
||||||
name: 'Update credit card',
|
|
||||||
description: 'Enter your credit card number',
|
|
||||||
panelLabel: 'Update',
|
|
||||||
token: submitToken,
|
|
||||||
image: 'static/img/quay-icon-stripe.png',
|
|
||||||
billingAddress: true,
|
|
||||||
zipCode: true,
|
|
||||||
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
|
|
||||||
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
planService.getEmail = function(orgname) {
|
|
||||||
var email = null;
|
|
||||||
if (UserService.currentUser()) {
|
|
||||||
email = UserService.currentUser().email;
|
|
||||||
|
|
||||||
if (orgname) {
|
|
||||||
org = UserService.getOrganization(orgname);
|
|
||||||
if (org) {
|
|
||||||
emaiil = org.email;
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
}
|
|
||||||
return email;
|
|
||||||
};
|
|
||||||
|
|
||||||
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title, opt_async) {
|
|
||||||
if (!Features.BILLING) { return; }
|
|
||||||
|
|
||||||
// If the async parameter is true and this is a browser that does not allow async popup of the
|
|
||||||
// Stripe dialog (such as Mobile Safari or IE), show a bootbox to show the dialog instead.
|
|
||||||
var isIE = navigator.appName.indexOf("Internet Explorer") != -1;
|
|
||||||
var isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/);
|
|
||||||
|
|
||||||
if (opt_async && (isIE || isMobileSafari)) {
|
|
||||||
bootbox.dialog({
|
|
||||||
"message": "Please click 'Subscribe' to continue",
|
|
||||||
"buttons": {
|
|
||||||
"subscribe": {
|
|
||||||
"label": "Subscribe",
|
|
||||||
"className": "btn-primary",
|
|
||||||
"callback": function() {
|
|
||||||
planService.showSubscribeDialog($scope, orgname, planId, callbacks, opt_title, false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"close": {
|
|
||||||
"label": "Cancel",
|
|
||||||
"className": "btn-default"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (callbacks['opening']) {
|
|
||||||
callbacks['opening']();
|
|
||||||
}
|
|
||||||
|
|
||||||
var submitted = false;
|
|
||||||
var submitToken = function(token) {
|
|
||||||
if (submitted) { return; }
|
|
||||||
submitted = true;
|
|
||||||
|
|
||||||
if (Config.MIXPANEL_KEY) {
|
|
||||||
mixpanel.track('plan_subscribe');
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.$apply(function() {
|
|
||||||
if (callbacks['started']) {
|
|
||||||
callbacks['started']();
|
|
||||||
}
|
|
||||||
planService.setSubscription(orgname, planId, callbacks['success'], callbacks['failure'], token);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
planService.getPlan(planId, function(planDetails) {
|
|
||||||
var email = planService.getEmail(orgname);
|
|
||||||
StripeCheckout.open({
|
|
||||||
key: KeyService.stripePublishableKey,
|
|
||||||
email: email,
|
|
||||||
amount: planDetails.price,
|
|
||||||
currency: 'usd',
|
|
||||||
name: 'Quay ' + planDetails.title + ' Subscription',
|
|
||||||
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
|
|
||||||
panelLabel: opt_title || 'Subscribe',
|
|
||||||
token: submitToken,
|
|
||||||
image: 'static/img/quay-icon-stripe.png',
|
|
||||||
billingAddress: true,
|
|
||||||
zipCode: true,
|
|
||||||
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
|
|
||||||
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return planService;
|
return planService;
|
||||||
|
@ -3605,8 +3605,8 @@ class TestUserCard(ApiTestCase):
|
|||||||
|
|
||||||
def test_setusercard_error(self):
|
def test_setusercard_error(self):
|
||||||
self.login(ADMIN_ACCESS_USER)
|
self.login(ADMIN_ACCESS_USER)
|
||||||
json = self.postJsonResponse(UserCard, data=dict(token="sometoken"), expected_code=402)
|
# token not a valid param anymore. This becomes a checkout session instead
|
||||||
assert "carderror" in json
|
json = self.postJsonResponse(UserCard, data=dict(token="sometoken"), expected_code=400)
|
||||||
|
|
||||||
|
|
||||||
class TestOrgCard(ApiTestCase):
|
class TestOrgCard(ApiTestCase):
|
||||||
|
Reference in New Issue
Block a user