""" Billing information, subscriptions, and plan information. """ import stripe from flask import request from app import billing from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action, related_user_resource, internal_only, Unauthorized, NotFound, require_user_admin, show_if, path_param, require_scope, abort) from endpoints.api.subscribe import subscribe, subscription_view from auth.permissions import AdministerOrganizationPermission from auth.auth_context import get_authenticated_user from auth import scopes from data import model from data.billing import PLANS, get_plan import features import uuid import json def get_namespace_plan(namespace): """ Returns the plan of the given namespace. """ namespace_user = model.user.get_namespace_user(namespace) if namespace_user is None: return None if not namespace_user.stripe_id: return None # Ask Stripe for the subscribed plan. # TODO: Can we cache this or make it faster somehow? try: cus = billing.Customer.retrieve(namespace_user.stripe_id) except stripe.APIConnectionError: abort(503, message='Cannot contact Stripe') if not cus.subscription: return None return get_plan(cus.subscription.plan.id) def lookup_allowed_private_repos(namespace): """ Returns false if the given namespace has used its allotment of private repositories. """ current_plan = get_namespace_plan(namespace) if current_plan is None: return False # Find the number of private repositories used by the namespace and compare it to the # plan subscribed. private_repos = model.user.get_private_repo_count(namespace) return private_repos < current_plan['privateRepos'] def carderror_response(e): return {'carderror': e.message}, 402 def get_card(user): card_info = { 'is_valid': False } if user.stripe_id: try: cus = billing.Customer.retrieve(user.stripe_id) except stripe.APIConnectionError as e: abort(503, message='Cannot contact Stripe') 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, 'exp_month': default_card.exp_month, 'exp_year': default_card.exp_year } return {'card': card_info} def set_card(user, token): if user.stripe_id: try: cus = billing.Customer.retrieve(user.stripe_id) except stripe.APIConnectionError as e: abort(503, message='Cannot contact Stripe') 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) except stripe.APIConnectionError as e: return carderror_response(e) 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 } try: invoices = billing.Invoice.all(customer=customer_id, count=12) except stripe.APIConnectionError as e: abort(503, message='Cannot contact Stripe') return { 'invoices': [invoice_view(i) for i in invoices.data] } def get_invoice_fields(user): try: cus = billing.Customer.retrieve(user.stripe_id) except stripe.APIConnectionError: abort(503, message='Cannot contact Stripe') if not 'metadata' in cus: cus.metadata = {} return json.loads(cus.metadata.get('invoice_fields') or '[]'), cus def create_billing_invoice_field(user, title, value): new_field = { 'uuid': str(uuid.uuid4()).split('-')[0], 'title': title, 'value': value } invoice_fields, cus = get_invoice_fields(user) invoice_fields.append(new_field) if not 'metadata' in cus: cus.metadata = {} cus.metadata['invoice_fields'] = json.dumps(invoice_fields) cus.save() return new_field def delete_billing_invoice_field(user, field_uuid): invoice_fields, cus = get_invoice_fields(user) invoice_fields = [field for field in invoice_fields if not field['uuid'] == field_uuid] if not 'metadata' in cus: cus.metadata = {} cus.metadata['invoice_fields'] = json.dumps(invoice_fields) cus.save() return True @resource('/v1/plans/') @show_if(features.BILLING) 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') @internal_only @show_if(features.BILLING) class UserCard(ApiResource): """ Resource for managing a user's credit card. """ schemas = { 'UserCard': { 'id': 'UserCard', 'type': 'object', 'description': 'Description of a user card', 'required': [ 'token', ], 'properties': { 'token': { 'type': 'string', 'description': 'Stripe token that is generated by stripe checkout.js', }, }, }, } @require_user_admin @nickname('getUserCard') def get(self): """ Get the user's credit card. """ user = get_authenticated_user() return get_card(user) @require_user_admin @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') @path_param('orgname', 'The name of the organization') @internal_only @related_user_resource(UserCard) @show_if(features.BILLING) class OrganizationCard(ApiResource): """ Resource for managing an organization's credit card. """ schemas = { 'OrgCard': { 'id': 'OrgCard', 'type': 'object', 'description': 'Description of a user card', 'required': [ 'token', ], 'properties': { 'token': { 'type': 'string', 'description': 'Stripe token that is generated by stripe checkout.js', }, }, }, } @require_scope(scopes.ORG_ADMIN) @nickname('getOrgCard') def get(self, orgname): """ Get the organization's credit card. """ permission = AdministerOrganizationPermission(orgname) if permission.can(): organization = model.organization.get_organization(orgname) return get_card(organization) raise Unauthorized() @nickname('setOrgCard') @validate_json_request('OrgCard') def post(self, orgname): """ Update the orgnaization's credit card. """ permission = AdministerOrganizationPermission(orgname) if permission.can(): organization = model.organization.get_organization(orgname) token = request.get_json()['token'] response = set_card(organization, token) log_action('account_change_cc', orgname) return response raise Unauthorized() @resource('/v1/user/plan') @internal_only @show_if(features.BILLING) class UserPlan(ApiResource): """ Resource for managing a user's subscription. """ schemas = { 'UserSubscription': { 'id': 'UserSubscription', 'type': 'object', 'description': 'Description of a user card', 'required': [ 'plan', ], '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', }, }, }, } @require_user_admin @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 @require_user_admin @nickname('getUserSubscription') def get(self): """ Fetch any existing subscription for the user. """ cus = None user = get_authenticated_user() private_repos = model.user.get_private_repo_count(user.username) if user.stripe_id: try: cus = billing.Customer.retrieve(user.stripe_id) except stripe.APIConnectionError as e: abort(503, message='Cannot contact Stripe') if cus.subscription: return subscription_view(cus.subscription, private_repos) return { 'hasSubscription': False, 'isExistingCustomer': cus is not None, 'plan': 'free', 'usedPrivateRepos': private_repos, } @resource('/v1/organization//plan') @path_param('orgname', 'The name of the organization') @internal_only @related_user_resource(UserPlan) @show_if(features.BILLING) class OrganizationPlan(ApiResource): """ Resource for managing a org's subscription. """ schemas = { 'OrgSubscription': { 'id': 'OrgSubscription', 'type': 'object', 'description': 'Description of a user card', 'required': [ 'plan', ], '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', }, }, }, } @require_scope(scopes.ORG_ADMIN) @nickname('updateOrgSubscription') @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.organization.get_organization(orgname) return subscribe(organization, plan, token, True) # Business plan required raise Unauthorized() @require_scope(scopes.ORG_ADMIN) @nickname('getOrgSubscription') def get(self, orgname): """ Fetch any existing subscription for the org. """ cus = None permission = AdministerOrganizationPermission(orgname) if permission.can(): private_repos = model.user.get_private_repo_count(orgname) organization = model.organization.get_organization(orgname) if organization.stripe_id: try: cus = billing.Customer.retrieve(organization.stripe_id) except stripe.APIConnectionError as e: abort(503, message='Cannot contact Stripe') if cus.subscription: return subscription_view(cus.subscription, private_repos) return { 'hasSubscription': False, 'isExistingCustomer': cus is not None, 'plan': 'free', 'usedPrivateRepos': private_repos, } raise Unauthorized() @resource('/v1/user/invoices') @internal_only @show_if(features.BILLING) class UserInvoiceList(ApiResource): """ Resource for listing a user's invoices. """ @require_user_admin @nickname('listUserInvoices') def get(self): """ List the invoices for the current user. """ user = get_authenticated_user() if not user.stripe_id: raise NotFound() return get_invoices(user.stripe_id) @resource('/v1/organization//invoices') @path_param('orgname', 'The name of the organization') @related_user_resource(UserInvoiceList) @show_if(features.BILLING) class OrganizationInvoiceList(ApiResource): """ Resource for listing an orgnaization's invoices. """ @require_scope(scopes.ORG_ADMIN) @nickname('listOrgInvoices') def get(self, orgname): """ List the invoices for the specified orgnaization. """ permission = AdministerOrganizationPermission(orgname) if permission.can(): organization = model.organization.get_organization(orgname) if not organization.stripe_id: raise NotFound() return get_invoices(organization.stripe_id) raise Unauthorized() @resource('/v1/user/invoice/fields') @internal_only @show_if(features.BILLING) class UserInvoiceFieldList(ApiResource): """ Resource for listing and creating a user's custom invoice fields. """ schemas = { 'InvoiceField': { 'id': 'InvoiceField', 'type': 'object', 'description': 'Description of an invoice field', 'required': [ 'title', 'value' ], 'properties': { 'title': { 'type': 'string', 'description': 'The title of the field being added', }, 'value': { 'type': 'string', 'description': 'The value of the field being added', }, }, }, } @require_user_admin @nickname('listUserInvoiceFields') def get(self): """ List the invoice fields for the current user. """ user = get_authenticated_user() if not user.stripe_id: raise NotFound() return {'fields': get_invoice_fields(user)[0]} @require_user_admin @nickname('createUserInvoiceField') @validate_json_request('InvoiceField') def post(self): """ Creates a new invoice field. """ user = get_authenticated_user() if not user.stripe_id: raise NotFound() data = request.get_json() created_field = create_billing_invoice_field(user, data['title'], data['value']) return created_field @resource('/v1/user/invoice/field/') @internal_only @show_if(features.BILLING) class UserInvoiceField(ApiResource): """ Resource for deleting a user's custom invoice fields. """ @require_user_admin @nickname('deleteUserInvoiceField') def delete(self, field_uuid): """ Deletes the invoice field for the current user. """ user = get_authenticated_user() if not user.stripe_id: raise NotFound() result = delete_billing_invoice_field(user, field_uuid) if not result: abort(404) return 'Okay', 201 @resource('/v1/organization//invoice/fields') @path_param('orgname', 'The name of the organization') @related_user_resource(UserInvoiceFieldList) @internal_only @show_if(features.BILLING) class OrganizationInvoiceFieldList(ApiResource): """ Resource for listing and creating an organization's custom invoice fields. """ schemas = { 'InvoiceField': { 'id': 'InvoiceField', 'type': 'object', 'description': 'Description of an invoice field', 'required': [ 'title', 'value' ], 'properties': { 'title': { 'type': 'string', 'description': 'The title of the field being added', }, 'value': { 'type': 'string', 'description': 'The value of the field being added', }, }, }, } @require_scope(scopes.ORG_ADMIN) @nickname('listOrgInvoiceFields') def get(self, orgname): """ List the invoice fields for the organization. """ permission = AdministerOrganizationPermission(orgname) if permission.can(): organization = model.organization.get_organization(orgname) if not organization.stripe_id: raise NotFound() return {'fields': get_invoice_fields(organization)[0]} abort(403) @require_scope(scopes.ORG_ADMIN) @nickname('createOrgInvoiceField') @validate_json_request('InvoiceField') def post(self, orgname): """ Creates a new invoice field. """ permission = AdministerOrganizationPermission(orgname) if permission.can(): organization = model.organization.get_organization(orgname) if not organization.stripe_id: raise NotFound() data = request.get_json() created_field = create_billing_invoice_field(organization, data['title'], data['value']) return created_field abort(403) @resource('/v1/organization//invoice/field/') @path_param('orgname', 'The name of the organization') @related_user_resource(UserInvoiceField) @internal_only @show_if(features.BILLING) class OrganizationInvoiceField(ApiResource): """ Resource for deleting an organization's custom invoice fields. """ @require_scope(scopes.ORG_ADMIN) @nickname('deleteOrgInvoiceField') def delete(self, orgname, field_uuid): """ Deletes the invoice field for the current user. """ permission = AdministerOrganizationPermission(orgname) if permission.can(): organization = model.organization.get_organization(orgname) if not organization.stripe_id: raise NotFound() result = delete_billing_invoice_field(organization, field_uuid) if not result: abort(404) return 'Okay', 201 abort(403)