Port over the billing apis.
This commit is contained in:
parent
a667714d3d
commit
3c268de025
4 changed files with 325 additions and 0 deletions
|
@ -173,6 +173,7 @@ def log_action(kind, user_or_orgname, metadata={}, repo=None):
|
||||||
|
|
||||||
import endpoints.api.legacy
|
import endpoints.api.legacy
|
||||||
|
|
||||||
|
import endpoints.api.billing
|
||||||
import endpoints.api.build
|
import endpoints.api.build
|
||||||
import endpoints.api.discovery
|
import endpoints.api.discovery
|
||||||
import endpoints.api.image
|
import endpoints.api.image
|
||||||
|
|
313
endpoints/api/billing.py
Normal file
313
endpoints/api/billing.py
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
import logging
|
||||||
|
import stripe
|
||||||
|
|
||||||
|
from flask import request
|
||||||
|
from flask.ext.restful import abort
|
||||||
|
|
||||||
|
from endpoints.api import resource, nickname, ApiResource, validate_json_request, log_action
|
||||||
|
from endpoints.api.subscribe import subscribe, subscription_view
|
||||||
|
from auth.permissions import AdministerOrganizationPermission
|
||||||
|
from auth.auth_context import get_authenticated_user
|
||||||
|
from data import model
|
||||||
|
from data.plans import PLANS
|
||||||
|
|
||||||
|
|
||||||
|
def carderror_response(e):
|
||||||
|
return {'carderror': e.message}, 402
|
||||||
|
|
||||||
|
|
||||||
|
def get_card(user):
|
||||||
|
card_info = {
|
||||||
|
'is_valid': False
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.stripe_id:
|
||||||
|
cus = stripe.Customer.retrieve(user.stripe_id)
|
||||||
|
if cus and cus.default_card:
|
||||||
|
# Find the default card.
|
||||||
|
default_card = None
|
||||||
|
for card in cus.cards.data:
|
||||||
|
if card.id == cus.default_card:
|
||||||
|
default_card = card
|
||||||
|
break
|
||||||
|
|
||||||
|
if default_card:
|
||||||
|
card_info = {
|
||||||
|
'owner': default_card.name,
|
||||||
|
'type': default_card.type,
|
||||||
|
'last4': default_card.last4
|
||||||
|
}
|
||||||
|
|
||||||
|
return {'card': card_info}
|
||||||
|
|
||||||
|
|
||||||
|
def set_card(user, token):
|
||||||
|
if user.stripe_id:
|
||||||
|
cus = stripe.Customer.retrieve(user.stripe_id)
|
||||||
|
if cus:
|
||||||
|
try:
|
||||||
|
cus.card = token
|
||||||
|
cus.save()
|
||||||
|
except stripe.CardError as exc:
|
||||||
|
return carderror_response(exc)
|
||||||
|
except stripe.InvalidRequestError as exc:
|
||||||
|
return carderror_response(exc)
|
||||||
|
|
||||||
|
return get_card(user)
|
||||||
|
|
||||||
|
|
||||||
|
def get_invoices(customer_id):
|
||||||
|
def invoice_view(i):
|
||||||
|
return {
|
||||||
|
'id': i.id,
|
||||||
|
'date': i.date,
|
||||||
|
'period_start': i.period_start,
|
||||||
|
'period_end': i.period_end,
|
||||||
|
'paid': i.paid,
|
||||||
|
'amount_due': i.amount_due,
|
||||||
|
'next_payment_attempt': i.next_payment_attempt,
|
||||||
|
'attempted': i.attempted,
|
||||||
|
'closed': i.closed,
|
||||||
|
'total': i.total,
|
||||||
|
'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None
|
||||||
|
}
|
||||||
|
|
||||||
|
invoices = stripe.Invoice.all(customer=customer_id, count=12)
|
||||||
|
return {
|
||||||
|
'invoices': [invoice_view(i) for i in invoices.data]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/plans/')
|
||||||
|
class ListPlans(ApiResource):
|
||||||
|
""" Resource for listing the available plans. """
|
||||||
|
@nickname('listPlans')
|
||||||
|
def get(self):
|
||||||
|
""" List the avaialble plans. """
|
||||||
|
return {
|
||||||
|
'plans': PLANS,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/user/card')
|
||||||
|
class UserCard(ApiResource):
|
||||||
|
""" Resource for managing a user's credit card. """
|
||||||
|
schemas = {
|
||||||
|
'UserCard': {
|
||||||
|
'id': 'UserCard',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Description of a user card',
|
||||||
|
'required': True,
|
||||||
|
'properties': {
|
||||||
|
'token': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Stripe token that is generated by stripe checkout.js',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@nickname('getUserCard')
|
||||||
|
def get(self):
|
||||||
|
""" Get the user's credit card. """
|
||||||
|
user = get_authenticated_user()
|
||||||
|
return get_card(user)
|
||||||
|
|
||||||
|
@nickname('setUserCard')
|
||||||
|
@validate_json_request('UserCard')
|
||||||
|
def post(self):
|
||||||
|
""" Update the user's credit card. """
|
||||||
|
user = get_authenticated_user()
|
||||||
|
token = request.get_json()['token']
|
||||||
|
response = set_card(user, token)
|
||||||
|
log_action('account_change_cc', user.username)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/organization/<orgname>/card')
|
||||||
|
class OrganizationCard(ApiResource):
|
||||||
|
""" Resource for managing an organization's credit card. """
|
||||||
|
schemas = {
|
||||||
|
'OrgCard': {
|
||||||
|
'id': 'OrgCard',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Description of a user card',
|
||||||
|
'required': True,
|
||||||
|
'properties': {
|
||||||
|
'token': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Stripe token that is generated by stripe checkout.js',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@nickname('getOrgCard')
|
||||||
|
# @org_api_call('getOrgCard')
|
||||||
|
def get(self, orgname):
|
||||||
|
""" Get the organization's credit card. """
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
organization = model.get_organization(orgname)
|
||||||
|
return get_card(organization)
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
@nickname('setOrgCard')
|
||||||
|
# @org_api_call('set_user_card')
|
||||||
|
@validate_json_request('OrgCard')
|
||||||
|
def post(self, orgname):
|
||||||
|
""" Update the orgnaization's credit card. """
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
organization = model.get_organization(orgname)
|
||||||
|
token = request.get_json()['token']
|
||||||
|
response = set_card(organization, token)
|
||||||
|
log_action('account_change_cc', orgname)
|
||||||
|
return response
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/user/plan')
|
||||||
|
class UserPlan(ApiResource):
|
||||||
|
""" Resource for managing a user's subscription. """
|
||||||
|
schemas = {
|
||||||
|
'UserSubscription': {
|
||||||
|
'id': 'UserSubscription',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Description of a user card',
|
||||||
|
'required': True,
|
||||||
|
'properties': {
|
||||||
|
'token': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Stripe token that is generated by stripe checkout.js',
|
||||||
|
},
|
||||||
|
'plan': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Plan name to which the user wants to subscribe',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@nickname('updateUserSubscription')
|
||||||
|
@validate_json_request('UserSubscription')
|
||||||
|
def put(self):
|
||||||
|
""" Create or update the user's subscription. """
|
||||||
|
request_data = request.get_json()
|
||||||
|
plan = request_data['plan']
|
||||||
|
token = request_data['token'] if 'token' in request_data else None
|
||||||
|
user = get_authenticated_user()
|
||||||
|
return subscribe(user, plan, token, False) # Business features not required
|
||||||
|
|
||||||
|
@nickname('getUserSubscription')
|
||||||
|
def get(self):
|
||||||
|
""" Fetch any existing subscription for the user. """
|
||||||
|
user = get_authenticated_user()
|
||||||
|
private_repos = model.get_private_repo_count(user.username)
|
||||||
|
|
||||||
|
if user.stripe_id:
|
||||||
|
cus = stripe.Customer.retrieve(user.stripe_id)
|
||||||
|
|
||||||
|
if cus.subscription:
|
||||||
|
return subscription_view(cus.subscription, private_repos)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'plan': 'free',
|
||||||
|
'usedPrivateRepos': private_repos,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/organization/<orgname>/plan')
|
||||||
|
class OrganizationPlan(ApiResource):
|
||||||
|
""" Resource for managing a org's subscription. """
|
||||||
|
schemas = {
|
||||||
|
'OrgSubscription': {
|
||||||
|
'id': 'OrgSubscription',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Description of a user card',
|
||||||
|
'required': True,
|
||||||
|
'properties': {
|
||||||
|
'token': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Stripe token that is generated by stripe checkout.js',
|
||||||
|
},
|
||||||
|
'plan': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Plan name to which the user wants to subscribe',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@nickname('updateOrgSubscription')
|
||||||
|
# @org_api_call('update_user_subscription')
|
||||||
|
@validate_json_request('OrgSubscription')
|
||||||
|
def put(self, orgname):
|
||||||
|
""" Create or update the org's subscription. """
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
request_data = request.get_json()
|
||||||
|
plan = request_data['plan']
|
||||||
|
token = request_data['token'] if 'token' in request_data else None
|
||||||
|
organization = model.get_organization(orgname)
|
||||||
|
return subscribe(organization, plan, token, True) # Business plan required
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
@nickname('getOrgSubscription')
|
||||||
|
# @org_api_call('get_user_subscription')
|
||||||
|
def get(self, orgname):
|
||||||
|
""" Fetch any existing subscription for the org. """
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
private_repos = model.get_private_repo_count(orgname)
|
||||||
|
organization = model.get_organization(orgname)
|
||||||
|
if organization.stripe_id:
|
||||||
|
cus = stripe.Customer.retrieve(organization.stripe_id)
|
||||||
|
|
||||||
|
if cus.subscription:
|
||||||
|
return subscription_view(cus.subscription, private_repos)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'plan': 'free',
|
||||||
|
'usedPrivateRepos': private_repos,
|
||||||
|
}
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/user/invoices')
|
||||||
|
class UserInvoiceList(ApiResource):
|
||||||
|
""" Resource for listing a user's invoices. """
|
||||||
|
@nickname('listUserInvoices')
|
||||||
|
def get(self):
|
||||||
|
""" List the invoices for the current user. """
|
||||||
|
user = get_authenticated_user()
|
||||||
|
if not user.stripe_id:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return get_invoices(user.stripe_id)
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/organization/<orgname>/invoices')
|
||||||
|
class OrgnaizationInvoiceList(ApiResource):
|
||||||
|
""" Resource for listing an orgnaization's invoices. """
|
||||||
|
@nickname('listOrgInvoices')
|
||||||
|
# @org_api_call('list_user_invoices')
|
||||||
|
def get(self, orgname):
|
||||||
|
""" List the invoices for the specified orgnaization. """
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
organization = model.get_organization(orgname)
|
||||||
|
if not organization.stripe_id:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return get_invoices(organization.stripe_id)
|
||||||
|
|
||||||
|
abort(403)
|
|
@ -2093,6 +2093,7 @@ def subscription_view(stripe_subscription, used_repos):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/user/card', methods=['GET'])
|
@api_bp.route('/user/card', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@internal_api_call
|
@internal_api_call
|
||||||
|
@ -2101,6 +2102,7 @@ def get_user_card():
|
||||||
return get_card(user)
|
return get_card(user)
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/organization/<orgname>/card', methods=['GET'])
|
@api_bp.route('/organization/<orgname>/card', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@internal_api_call
|
@internal_api_call
|
||||||
|
@ -2114,6 +2116,7 @@ def get_org_card(orgname):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/user/card', methods=['POST'])
|
@api_bp.route('/user/card', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@internal_api_call
|
@internal_api_call
|
||||||
|
@ -2125,6 +2128,7 @@ def set_user_card():
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/organization/<orgname>/card', methods=['POST'])
|
@api_bp.route('/organization/<orgname>/card', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@org_api_call('set_user_card')
|
@org_api_call('set_user_card')
|
||||||
|
@ -2179,6 +2183,7 @@ def get_card(user):
|
||||||
|
|
||||||
return jsonify({'card': card_info})
|
return jsonify({'card': card_info})
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/user/plan', methods=['PUT'])
|
@api_bp.route('/user/plan', methods=['PUT'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@internal_api_call
|
@internal_api_call
|
||||||
|
@ -2272,6 +2277,7 @@ def subscribe(user, plan, token, require_business_plan):
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/user/invoices', methods=['GET'])
|
@api_bp.route('/user/invoices', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def list_user_invoices():
|
def list_user_invoices():
|
||||||
|
@ -2282,6 +2288,7 @@ def list_user_invoices():
|
||||||
return get_invoices(user.stripe_id)
|
return get_invoices(user.stripe_id)
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/organization/<orgname>/invoices', methods=['GET'])
|
@api_bp.route('/organization/<orgname>/invoices', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@org_api_call('list_user_invoices')
|
@org_api_call('list_user_invoices')
|
||||||
|
@ -2319,6 +2326,7 @@ def get_invoices(customer_id):
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/organization/<orgname>/plan', methods=['PUT'])
|
@api_bp.route('/organization/<orgname>/plan', methods=['PUT'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@internal_api_call
|
@internal_api_call
|
||||||
|
@ -2335,6 +2343,7 @@ def update_org_subscription(orgname):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/user/plan', methods=['GET'])
|
@api_bp.route('/user/plan', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@internal_api_call
|
@internal_api_call
|
||||||
|
@ -2354,6 +2363,7 @@ def get_user_subscription():
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/organization/<orgname>/plan', methods=['GET'])
|
@api_bp.route('/organization/<orgname>/plan', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@internal_api_call
|
@internal_api_call
|
||||||
|
|
|
@ -157,6 +157,7 @@ class OrgPrivateRepositories(ApiResource):
|
||||||
# @org_api_call('get_user_private_allowed')
|
# @org_api_call('get_user_private_allowed')
|
||||||
@nickname('getOrganizationPrivateAllowed')
|
@nickname('getOrganizationPrivateAllowed')
|
||||||
def get(self, orgname):
|
def get(self, orgname):
|
||||||
|
""" Return whether or not this org is allowed to create new private repositories. """
|
||||||
permission = CreateRepositoryPermission(orgname)
|
permission = CreateRepositoryPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
organization = model.get_organization(orgname)
|
organization = model.get_organization(orgname)
|
||||||
|
|
Reference in a new issue