diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index 60d4b95d9..eedb94e93 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -173,6 +173,7 @@ def log_action(kind, user_or_orgname, metadata={}, repo=None): import endpoints.api.legacy +import endpoints.api.billing import endpoints.api.build import endpoints.api.discovery import endpoints.api.image diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py new file mode 100644 index 000000000..52ee6d351 --- /dev/null +++ b/endpoints/api/billing.py @@ -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//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//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//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) \ No newline at end of file diff --git a/endpoints/api/legacy.py b/endpoints/api/legacy.py index bb34b5918..674084b7e 100644 --- a/endpoints/api/legacy.py +++ b/endpoints/api/legacy.py @@ -2093,6 +2093,7 @@ def subscription_view(stripe_subscription, used_repos): } +# Ported @api_bp.route('/user/card', methods=['GET']) @api_login_required @internal_api_call @@ -2101,6 +2102,7 @@ def get_user_card(): return get_card(user) +# Ported @api_bp.route('/organization//card', methods=['GET']) @api_login_required @internal_api_call @@ -2114,6 +2116,7 @@ def get_org_card(orgname): abort(403) +# Ported @api_bp.route('/user/card', methods=['POST']) @api_login_required @internal_api_call @@ -2125,6 +2128,7 @@ def set_user_card(): return response +# Ported @api_bp.route('/organization//card', methods=['POST']) @api_login_required @org_api_call('set_user_card') @@ -2179,6 +2183,7 @@ def get_card(user): return jsonify({'card': card_info}) +# Ported @api_bp.route('/user/plan', methods=['PUT']) @api_login_required @internal_api_call @@ -2272,6 +2277,7 @@ def subscribe(user, plan, token, require_business_plan): return resp +# Ported @api_bp.route('/user/invoices', methods=['GET']) @api_login_required def list_user_invoices(): @@ -2282,6 +2288,7 @@ def list_user_invoices(): return get_invoices(user.stripe_id) +# Ported @api_bp.route('/organization//invoices', methods=['GET']) @api_login_required @org_api_call('list_user_invoices') @@ -2319,6 +2326,7 @@ def get_invoices(customer_id): }) +# Ported @api_bp.route('/organization//plan', methods=['PUT']) @api_login_required @internal_api_call @@ -2335,6 +2343,7 @@ def update_org_subscription(orgname): abort(403) +# Ported @api_bp.route('/user/plan', methods=['GET']) @api_login_required @internal_api_call @@ -2354,6 +2363,7 @@ def get_user_subscription(): }) +# Ported @api_bp.route('/organization//plan', methods=['GET']) @api_login_required @internal_api_call diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index 059c4711f..5a68efae7 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -157,6 +157,7 @@ class OrgPrivateRepositories(ApiResource): # @org_api_call('get_user_private_allowed') @nickname('getOrganizationPrivateAllowed') def get(self, orgname): + """ Return whether or not this org is allowed to create new private repositories. """ permission = CreateRepositoryPermission(orgname) if permission.can(): organization = model.get_organization(orgname)