Add support for org-based subscriptions

This commit is contained in:
Joseph Schorr 2013-11-06 14:19:56 -05:00
parent 97a7cd23e7
commit e356a10378
5 changed files with 109 additions and 35 deletions

View file

@ -1076,19 +1076,19 @@ def subscription_view(stripe_subscription, used_repos):
@app.route('/api/user/plan', methods=['PUT']) @app.route('/api/user/plan', methods=['PUT'])
@api_login_required @api_login_required
def subscribe(): def subscribe_api():
# Amount in cents
amount = 500
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 = current_user.db_user() user = current_user.db_user()
return subscribe(user, plan, token)
def subscribe(user, plan, token = None):
private_repos = model.get_private_repo_count(user.username) private_repos = model.get_private_repo_count(user.username)
if not user.stripe_id: if not user.stripe_id:
# Create the customer and plan simultaneously # Create the customer and plan simultaneously
card = request_data['token'] card = token
cus = stripe.Customer.create(email=user.email, plan=plan, card=card) cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
user.stripe_id = cus.id user.stripe_id = cus.id
user.save() user.save()
@ -1112,18 +1112,30 @@ def subscribe():
else: else:
cus.plan = plan cus.plan = plan
# User may have been a previous customer who is resubscribing # User may have been a previous customer who is resubscribing
if 'token' in request_data: if token:
cus.card = request_data['token'] cus.card = token
cus.save() cus.save()
response_json = subscription_view(cus.subscription, private_repos) response_json = subscription_view(cus.subscription, private_repos)
return jsonify(response_json) return jsonify(response_json)
@app.route('/api/organization/<orgname>/plan', methods=['PUT'])
@api_login_required
def subscribe_org_api(orgname):
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)
abort(403)
@app.route('/api/user/plan', methods=['GET']) @app.route('/api/user/plan', methods=['GET'])
@api_login_required @api_login_required
def get_subscription(): def get_subscription():
@ -1140,3 +1152,24 @@ def get_subscription():
'plan': 'free', 'plan': 'free',
'usedPrivateRepos': private_repos, 'usedPrivateRepos': private_repos,
}) })
@app.route('/api/organization/<orgname>/plan', methods=['GET'])
@api_login_required
def get_org_subscription(orgname):
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 jsonify(subscription_view(cus.subscription, private_repos))
return jsonify({
'plan': 'bus-free',
'usedPrivateRepos': private_repos,
})
abort(403)

View file

@ -2,6 +2,10 @@
font-family: 'Droid Sans', sans-serif; font-family: 'Droid Sans', sans-serif;
} }
.button-hidden {
visibility: hidden;
}
.organization-header-element { .organization-header-element {
padding: 20px; padding: 20px;
margin-bottom: 20px; margin-bottom: 20px;
@ -1242,10 +1246,6 @@ p.editable:hover i {
text-align: center; text-align: center;
} }
.user-admin .panel-plan .button-hidden {
visibility: hidden;
}
.user-admin .plan-description { .user-admin .plan-description {
font-size: 1.2em; font-size: 1.2em;
margin-bottom: 10px; margin-bottom: 10px;

View file

@ -31,6 +31,17 @@ function getFirstTextLine(commentString) {
return ''; return '';
} }
function getRestUrl(args) {
var url = '';
for (var i = 0; i < arguments.length; ++i) {
if (i > 0) {
url += '/';
}
url += encodeURI(arguments[i])
}
return url;
}
function getMarkedDown(string) { function getMarkedDown(string) {
return Markdown.getSanitizingConverter().makeHtml(string || ''); return Markdown.getSanitizingConverter().makeHtml(string || '');
} }
@ -160,7 +171,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
}); });
}; };
planService.showSubscribeDialog = function($scope, planId, started, success, failed) { planService.showSubscribeDialog = function($scope, planId, orgname, started, success, failed) {
var submitToken = function(token) { var submitToken = function(token) {
$scope.$apply(function() { $scope.$apply(function() {
started(); started();
@ -173,7 +184,8 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
plan: planId, plan: planId,
}; };
var createSubscriptionRequest = Restangular.one('user/plan'); var url = orgname ? getRestUrl('organization', orgname, 'plan') : 'user/plan';
var createSubscriptionRequest = Restangular.one(url);
$scope.$apply(function() { $scope.$apply(function() {
createSubscriptionRequest.customPUT(subscriptionDetails).then(success, failed); createSubscriptionRequest.customPUT(subscriptionDetails).then(success, failed);
}); });

View file

@ -14,17 +14,6 @@ $.fn.clipboardCopy = function() {
}); });
}; };
function getRestUrl(args) {
var url = '';
for (var i = 0; i < arguments.length; ++i) {
if (i > 0) {
url += '/';
}
url += encodeURI(arguments[i])
}
return url;
}
function HeaderCtrl($scope, $location, UserService, Restangular) { function HeaderCtrl($scope, $location, UserService, Restangular) {
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
$scope.user = currentUser; $scope.user = currentUser;
@ -716,7 +705,7 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService,
$scope.planChanging = false; $scope.planChanging = false;
$scope.subscribe = function(planId) { $scope.subscribe = function(planId) {
PlanService.showSubscribeDialog($scope, planId, function() { PlanService.showSubscribeDialog($scope, planId, null, function() {
// Subscribing. // Subscribing.
$scope.planChanging = true; $scope.planChanging = true;
}, function(plan) { }, function(plan) {
@ -1035,7 +1024,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
}; };
$scope.upgradePlan = function() { $scope.upgradePlan = function() {
PlanService.showSubscribeDialog($scope, $scope.planRequired.stripeId, function() { PlanService.showSubscribeDialog($scope, $scope.planRequired.stripeId, null, function() {
// Subscribing. // Subscribing.
$scope.planChanging = true; $scope.planChanging = true;
}, function(plan) { }, function(plan) {
@ -1203,6 +1192,7 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
}; };
var subscribedToPlan = function(sub) { var subscribedToPlan = function(sub) {
$scope.planChanging = false;
$scope.subscription = sub; $scope.subscription = sub;
PlanService.getPlan(sub.plan, function(subscribedPlan) { PlanService.getPlan(sub.plan, function(subscribedPlan) {
$scope.subscribedPlan = subscribedPlan; $scope.subscribedPlan = subscribedPlan;
@ -1223,9 +1213,11 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
var loadSubscription = function() { var loadSubscription = function() {
$scope.planLoading = true; $scope.planLoading = true;
UserService.getCurrentSubscription(subscribedToPlan, function() {
var getSubscription = Restangular.one(getRestUrl('organization', orgname, 'plan'));
getSubscription.get().then(subscribedToPlan, function() {
// Organization has no subscription. // Organization has no subscription.
$scope.planLoading = false; subscribedToPlan({'plan': 'bus-free'});
}); });
}; };
@ -1235,6 +1227,38 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
return 'success'; return 'success';
}; };
$scope.subscribe = function(planId) {
$scope.planChanging = true;
PlanService.showSubscribeDialog($scope, planId, orgname, function() {
// Subscribing.
}, function(plan) {
// Subscribed.
subscribedToPlan(plan);
}, function() {
// Failure.
$scope.planChanging = false;
});
};
$scope.changeSubscription = function(planId) {
$scope.planChanging = true;
$scope.errorMessage = undefined;
var subscriptionDetails = {
plan: planId,
};
var changeSubscriptionRequest = Restangular.one(getRestUrl('organization', orgname, 'plan'));
changeSubscriptionRequest.customPUT(subscriptionDetails).then(subscribedToPlan, function() {
// Failure
$scope.planChanging = false;
});
};
$scope.cancelSubscription = function() {
$scope.changeSubscription('bus-free');
};
loadSubscription(); loadSubscription();
loadOrganization(); loadOrganization();
} }

View file

@ -70,7 +70,9 @@
upgrading your subscription to avoid future disruptions in your organization's service. upgrading your subscription to avoid future disruptions in your organization's service.
</div> </div>
<table class="table table-hover plans-table"> <i class="fa fa-spinner fa-spin fa-3x" ng-show="planChanging"></i>
<table class="table table-hover plans-table" ng-show="!planChanging">
<thead> <thead>
<td>Plan</td> <td>Plan</td>
<td>Private Repositories</td> <td>Private Repositories</td>
@ -84,12 +86,15 @@
<td><div class="plan-price">${{ plan.price / 100 }}</div></td> <td><div class="plan-price">${{ plan.price / 100 }}</div></td>
<td class="controls"> <td class="controls">
<div ng-switch='plan.stripeId'> <div ng-switch='plan.stripeId'>
<div ng-switch-when='free'> <div ng-switch-when='bus-free'>
<button class="btn button-hidden">Hidden!</button> <button class="btn button-hidden">Hidden!</button>
</div> </div>
<div ng-switch-default> <div ng-switch-default>
<button class="btn btn-primary" ng-show="subscription.plan === 'free'" ng-click="subscribe(plan.stripeId)">Subscribe</button> <button class="btn btn-primary" ng-show="subscription.plan === 'bus-free'" ng-click="subscribe(plan.stripeId)">Subscribe</button>
<button class="btn btn-default" ng-hide="subscription.plan === 'free' || subscription.plan === plan.stripeId" ng-click="changeSubscription(plan.stripeId)">Change</button> <button class="btn btn-default" ng-hide="subscription.plan === 'bus-free' || subscription.plan === plan.stripeId"
ng-click="changeSubscription(plan.stripeId)">
Change
</button>
<button class="btn btn-danger" ng-show="subscription.plan === plan.stripeId" ng-click="cancelSubscription()">Cancel</button> <button class="btn btn-danger" ng-show="subscription.plan === plan.stripeId" ng-click="cancelSubscription()">Cancel</button>
</div> </div>
</div> </div>