Fix so that credit card issues are displayed to the user
This commit is contained in:
parent
c7355f5509
commit
b0ac7883e3
6 changed files with 99 additions and 21 deletions
|
@ -1283,7 +1283,7 @@ def subscription_view(stripe_subscription, used_repos):
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def get_user_card_api():
|
def get_user_card_api():
|
||||||
user = current_user.db_user()
|
user = current_user.db_user()
|
||||||
return jsonify(get_card(user))
|
return get_card(user)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>/card', methods=['GET'])
|
@app.route('/api/organization/<orgname>/card', methods=['GET'])
|
||||||
|
@ -1292,7 +1292,7 @@ def get_org_card_api(orgname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
organization = model.get_organization(orgname)
|
organization = model.get_organization(orgname)
|
||||||
return jsonify(get_card(organization))
|
return get_card(organization)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
@ -1302,7 +1302,7 @@ def get_org_card_api(orgname):
|
||||||
def set_user_card_api():
|
def set_user_card_api():
|
||||||
user = current_user.db_user()
|
user = current_user.db_user()
|
||||||
token = request.get_json()['token']
|
token = request.get_json()['token']
|
||||||
return jsonify(set_card(user, token))
|
return set_card(user, token)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>/card', methods=['POST'])
|
@app.route('/api/organization/<orgname>/card', methods=['POST'])
|
||||||
|
@ -1318,13 +1318,14 @@ def set_org_card_api(orgname):
|
||||||
|
|
||||||
|
|
||||||
def set_card(user, token):
|
def set_card(user, token):
|
||||||
print token
|
|
||||||
|
|
||||||
if user.stripe_id:
|
if user.stripe_id:
|
||||||
cus = stripe.Customer.retrieve(user.stripe_id)
|
cus = stripe.Customer.retrieve(user.stripe_id)
|
||||||
if cus:
|
if cus:
|
||||||
cus.card = token
|
try:
|
||||||
cus.save()
|
cus.card = token
|
||||||
|
cus.save()
|
||||||
|
except stripe.CardError as e:
|
||||||
|
return carderror_response(e)
|
||||||
|
|
||||||
return get_card(user)
|
return get_card(user)
|
||||||
|
|
||||||
|
@ -1351,7 +1352,7 @@ def get_card(user):
|
||||||
'last4': card.last4
|
'last4': card.last4
|
||||||
}
|
}
|
||||||
|
|
||||||
return {'card': card_info}
|
return jsonify({'card': card_info})
|
||||||
|
|
||||||
@app.route('/api/user/plan', methods=['PUT'])
|
@app.route('/api/user/plan', methods=['PUT'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
@ -1363,6 +1364,14 @@ def subscribe_api():
|
||||||
return subscribe(user, plan, token, USER_PLANS)
|
return subscribe(user, plan, token, USER_PLANS)
|
||||||
|
|
||||||
|
|
||||||
|
def carderror_response(e):
|
||||||
|
resp = jsonify({
|
||||||
|
'carderror': e.message,
|
||||||
|
})
|
||||||
|
resp.status_code = 402
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
def subscribe(user, plan, token, accepted_plans):
|
def subscribe(user, plan, token, accepted_plans):
|
||||||
plan_found = None
|
plan_found = None
|
||||||
for plan_obj in accepted_plans:
|
for plan_obj in accepted_plans:
|
||||||
|
@ -1387,9 +1396,13 @@ def subscribe(user, plan, token, accepted_plans):
|
||||||
# They want a real paying plan, create the customer and plan
|
# They want a real paying plan, create the customer and plan
|
||||||
# simultaneously
|
# simultaneously
|
||||||
card = token
|
card = token
|
||||||
cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
|
|
||||||
user.stripe_id = cus.id
|
try:
|
||||||
user.save()
|
cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
|
||||||
|
user.stripe_id = cus.id
|
||||||
|
user.save()
|
||||||
|
except stripe.CardError as e:
|
||||||
|
return carderror_response(e)
|
||||||
|
|
||||||
response_json = subscription_view(cus.subscription, private_repos)
|
response_json = subscription_view(cus.subscription, private_repos)
|
||||||
status_code = 201
|
status_code = 201
|
||||||
|
@ -1405,12 +1418,17 @@ def subscribe(user, plan, token, accepted_plans):
|
||||||
cus.save()
|
cus.save()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
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:
|
if token:
|
||||||
cus.card = token
|
cus.card = token
|
||||||
|
|
||||||
cus.save()
|
cus.plan = plan
|
||||||
|
|
||||||
|
try:
|
||||||
|
cus.save()
|
||||||
|
except stripe.CardError as e:
|
||||||
|
return carderror_response(e)
|
||||||
|
|
||||||
response_json = subscription_view(cus.subscription, private_repos)
|
response_json = subscription_view(cus.subscription, private_repos)
|
||||||
|
|
||||||
resp = jsonify(response_json)
|
resp = jsonify(response_json)
|
||||||
|
|
|
@ -36,4 +36,22 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="cannotchangecardModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Cannot change credit card</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
Your credit card could not be changed
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -137,7 +137,26 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
planService.handleCardError = function(resp) {
|
||||||
|
if (!planService.isCardError(resp)) { return; }
|
||||||
|
|
||||||
|
bootbox.dialog({
|
||||||
|
"message": resp.data.carderror,
|
||||||
|
"title": "Credit card issue",
|
||||||
|
"buttons": {
|
||||||
|
"close": {
|
||||||
|
"label": "Close",
|
||||||
|
"className": "btn-primary"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
planService.isCardError = function(resp) {
|
||||||
|
return resp && resp.data && resp.data.carderror;
|
||||||
|
};
|
||||||
|
|
||||||
planService.verifyLoaded = function(callback) {
|
planService.verifyLoaded = function(callback) {
|
||||||
if (plans) {
|
if (plans) {
|
||||||
callback(plans);
|
callback(plans);
|
||||||
|
@ -259,7 +278,10 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
planService.setSubscription(orgname, planId, callbacks['success'], callbacks['failure']);
|
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
|
||||||
|
planService.handleCardError(resp);
|
||||||
|
callbacks['failure'](resp);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -269,7 +291,10 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an
|
||||||
callbacks['opening']();
|
callbacks['opening']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var submitted = false;
|
||||||
var submitToken = function(token) {
|
var submitToken = function(token) {
|
||||||
|
if (submitted) { return; }
|
||||||
|
submitted = true;
|
||||||
$scope.$apply(function() {
|
$scope.$apply(function() {
|
||||||
if (callbacks['started']) {
|
if (callbacks['started']) {
|
||||||
callbacks['started']();
|
callbacks['started']();
|
||||||
|
@ -281,7 +306,10 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an
|
||||||
|
|
||||||
var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card';
|
var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card';
|
||||||
var changeCardRequest = Restangular.one(url);
|
var changeCardRequest = Restangular.one(url);
|
||||||
changeCardRequest.customPOST(cardInfo).then(callbacks['success'], callbacks['failure']);
|
changeCardRequest.customPOST(cardInfo).then(callbacks['success'], function(resp) {
|
||||||
|
planService.handleCardError(resp);
|
||||||
|
callbacks['failure'](resp);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -321,7 +349,11 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an
|
||||||
callbacks['opening']();
|
callbacks['opening']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var submitted = false;
|
||||||
var submitToken = function(token) {
|
var submitToken = function(token) {
|
||||||
|
if (submitted) { return; }
|
||||||
|
submitted = true;
|
||||||
|
|
||||||
mixpanel.track('plan_subscribe');
|
mixpanel.track('plan_subscribe');
|
||||||
|
|
||||||
$scope.$apply(function() {
|
$scope.$apply(function() {
|
||||||
|
@ -823,6 +855,7 @@ quayApp.directive('billingOptions', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.changeCard = function() {
|
$scope.changeCard = function() {
|
||||||
|
var previousCard = $scope.currentCard;
|
||||||
$scope.changingCard = true;
|
$scope.changingCard = true;
|
||||||
var callbacks = {
|
var callbacks = {
|
||||||
'opened': function() { $scope.changingCard = true; },
|
'opened': function() { $scope.changingCard = true; },
|
||||||
|
@ -832,9 +865,13 @@ quayApp.directive('billingOptions', function () {
|
||||||
$scope.currentCard = resp.card;
|
$scope.currentCard = resp.card;
|
||||||
$scope.changingCard = false;
|
$scope.changingCard = false;
|
||||||
},
|
},
|
||||||
'failure': function() {
|
'failure': function(resp) {
|
||||||
$('#couldnotchangecardModal').modal({});
|
|
||||||
$scope.changingCard = false;
|
$scope.changingCard = false;
|
||||||
|
$scope.currentCard = previousCard;
|
||||||
|
|
||||||
|
if (!PlanService.isCardError(resp)) {
|
||||||
|
$('#cannotchangecardModal').modal({});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -925,7 +962,9 @@ quayApp.directive('planManager', function () {
|
||||||
'opened': function() { $scope.planChanging = true; },
|
'opened': function() { $scope.planChanging = true; },
|
||||||
'closed': function() { $scope.planChanging = false; },
|
'closed': function() { $scope.planChanging = false; },
|
||||||
'success': subscribedToPlan,
|
'success': subscribedToPlan,
|
||||||
'failure': function() { $scope.planChanging = false; }
|
'failure': function(resp) {
|
||||||
|
$scope.planChanging = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
PlanService.changePlan($scope, $scope.organization, planId, callbacks);
|
PlanService.changePlan($scope, $scope.organization, planId, callbacks);
|
||||||
|
|
|
@ -1008,7 +1008,10 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
|
||||||
'opened': function() { $scope.planChanging = true; },
|
'opened': function() { $scope.planChanging = true; },
|
||||||
'closed': function() { $scope.planChanging = false; },
|
'closed': function() { $scope.planChanging = false; },
|
||||||
'success': subscribedToPlan,
|
'success': subscribedToPlan,
|
||||||
'failure': function() { $('#couldnotsubscribeModal').modal(); $scope.planChanging = false; }
|
'failure': function(resp) {
|
||||||
|
$('#couldnotsubscribeModal').modal();
|
||||||
|
$scope.planChanging = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
PlanService.changePlan($scope, null, $scope.planRequired.stripeId, callbacks);
|
PlanService.changePlan($scope, null, $scope.planRequired.stripeId, callbacks);
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
|
|
||||||
<script src="//code.jquery.com/jquery.js"></script>
|
<script src="//code.jquery.com/jquery.js"></script>
|
||||||
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
|
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
|
||||||
|
<script src="//cdnjs.cloudflare.com/ajax/libs/bootbox.js/4.0.0/bootbox.min.js"></script>
|
||||||
|
|
||||||
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.min.js"></script>
|
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.min.js"></script>
|
||||||
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-route.min.js"></script>
|
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-route.min.js"></script>
|
||||||
|
|
|
@ -31,7 +31,6 @@
|
||||||
{% block body_content %}
|
{% block body_content %}
|
||||||
<div ng-view></div>
|
<div ng-view></div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
<!-- Modal message dialog -->
|
||||||
<div class="modal fade" id="sessionexpiredModal" data-backdrop="static">
|
<div class="modal fade" id="sessionexpiredModal" data-backdrop="static">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
|
Reference in a new issue