mirror of
https://github.com/quay/quay.git
synced 2025-07-28 20:22:05 +03:00
billing: marketplace UI (PROJQUAY-6551) (#2595)
* billing: marketplace UI adds UI in billing section for managing user and org-bound skus add more unit tests for org binding changed endpoint for bulk attaching skus to orgs
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
from calendar import timegm
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict
|
||||
@ -292,6 +293,7 @@ PLANS = [
|
||||
"price": 45000,
|
||||
"privateRepos": 250,
|
||||
"rh_sku": "MW00589MO",
|
||||
"billing_enabled": False,
|
||||
"stripeId": "bus-xlarge-2018",
|
||||
"audience": "For extra large businesses",
|
||||
"bus_features": True,
|
||||
@ -305,6 +307,7 @@ PLANS = [
|
||||
"price": 85000,
|
||||
"privateRepos": 500,
|
||||
"rh_sku": "MW00590MO",
|
||||
"billing_enabled": False,
|
||||
"stripeId": "bus-500-2018",
|
||||
"audience": "For huge business",
|
||||
"bus_features": True,
|
||||
@ -354,13 +357,21 @@ PLANS = [
|
||||
"plans_page_hidden": False,
|
||||
},
|
||||
{
|
||||
"title": "subscriptionwatch",
|
||||
"title": "premium",
|
||||
"privateRepos": 100,
|
||||
"stripeId": "not_a_stripe_plan",
|
||||
"rh_sku": "MW02701",
|
||||
"sku_billing": True,
|
||||
"plans_page_hidden": True,
|
||||
},
|
||||
{
|
||||
"title": "selfsupport",
|
||||
"privateRepos": sys.maxsize,
|
||||
"stripeId": "not_a_stripe_plan",
|
||||
"rh_sku": "MW02702",
|
||||
"sku_billing": True,
|
||||
"plans_page_hidden": True,
|
||||
},
|
||||
]
|
||||
|
||||
RH_SKUS = [
|
||||
@ -389,6 +400,8 @@ def get_plan_using_rh_sku(sku):
|
||||
"""
|
||||
Returns the plan with given sku or None if none.
|
||||
"""
|
||||
if sku is None:
|
||||
return None
|
||||
for plan in PLANS:
|
||||
if plan.get("rh_sku") == sku:
|
||||
return plan
|
||||
|
@ -955,9 +955,11 @@ class OrganizationRhSku(ApiResource):
|
||||
if query:
|
||||
subscriptions = list(query.dicts())
|
||||
for subscription in subscriptions:
|
||||
subscription["sku"] = marketplace_subscriptions.get_subscription_sku(
|
||||
subscription_sku = marketplace_subscriptions.get_subscription_sku(
|
||||
subscription["subscription_id"]
|
||||
)
|
||||
subscription["sku"] = subscription_sku
|
||||
subscription["metadata"] = get_plan_using_rh_sku(subscription_sku)
|
||||
return subscriptions
|
||||
else:
|
||||
return []
|
||||
@ -971,35 +973,71 @@ class OrganizationRhSku(ApiResource):
|
||||
"""
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
request_data = request.get_json()
|
||||
subscription_id = request_data["subscription_id"]
|
||||
organization = model.organization.get_organization(orgname)
|
||||
subscriptions = request_data["subscriptions"]
|
||||
if permission.can():
|
||||
organization = model.organization.get_organization(orgname)
|
||||
user = get_authenticated_user()
|
||||
account_number = marketplace_users.get_account_number(user)
|
||||
subscriptions = marketplace_subscriptions.get_list_of_subscriptions(account_number)
|
||||
for subscription in subscriptions:
|
||||
subscription_id = subscription.get("subscription_id")
|
||||
if subscription_id is None:
|
||||
break
|
||||
user = get_authenticated_user()
|
||||
account_number = marketplace_users.get_account_number(user)
|
||||
subscriptions = marketplace_subscriptions.get_list_of_subscriptions(account_number)
|
||||
|
||||
if subscriptions is None:
|
||||
abort(401, message="no valid subscriptions present")
|
||||
if subscriptions is None:
|
||||
abort(401, message="no valid subscriptions present")
|
||||
|
||||
user_subscription_ids = [int(subscription["id"]) for subscription in subscriptions]
|
||||
if int(subscription_id) in user_subscription_ids:
|
||||
try:
|
||||
model.organization_skus.bind_subscription_to_org(
|
||||
user_id=user.id, subscription_id=subscription_id, org_id=organization.id
|
||||
user_subscription_ids = [int(subscription["id"]) for subscription in subscriptions]
|
||||
if int(subscription_id) in user_subscription_ids:
|
||||
try:
|
||||
model.organization_skus.bind_subscription_to_org(
|
||||
user_id=user.id, subscription_id=subscription_id, org_id=organization.id
|
||||
)
|
||||
except model.OrgSubscriptionBindingAlreadyExists:
|
||||
abort(400, message="subscription is already bound to an org")
|
||||
else:
|
||||
abort(
|
||||
401,
|
||||
message=f"subscription {subscription_id} does not belong to {user.username}",
|
||||
)
|
||||
return "Okay", 201
|
||||
except model.OrgSubscriptionBindingAlreadyExists:
|
||||
abort(400, message="subscription is already bound to an org")
|
||||
else:
|
||||
abort(401, message=f"subscription does not belong to {user.username}")
|
||||
|
||||
return "Okay", 201
|
||||
|
||||
abort(401)
|
||||
|
||||
|
||||
@resource("/v1/organization/<orgname>/marketplace/batchremove")
|
||||
@path_param("orgname", "The name of the organization")
|
||||
@show_if(features.BILLING)
|
||||
class OrganizationRhSkuBatchRemoval(ApiResource):
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname("batchRemoveSku")
|
||||
def post(self, orgname):
|
||||
"""
|
||||
Batch remove skus from org
|
||||
"""
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
request_data = request.get_json()
|
||||
subscriptions = request_data["subscriptions"]
|
||||
if permission.can():
|
||||
try:
|
||||
organization = model.organization.get_organization(orgname)
|
||||
except InvalidOrganizationException:
|
||||
return ("Organization not valid", 400)
|
||||
for subscription in subscriptions:
|
||||
subscription_id = int(subscription.get("subscription_id"))
|
||||
if subscription_id is None:
|
||||
break
|
||||
model.organization_skus.remove_subscription_from_org(
|
||||
organization.id, subscription_id
|
||||
)
|
||||
return ("Deleted", 204)
|
||||
abort(401)
|
||||
|
||||
|
||||
@resource("/v1/organization/<orgname>/marketplace/<subscription_id>")
|
||||
@path_param("orgname", "The name of the organization")
|
||||
@path_param("subscription_id", "Marketplace subscription id")
|
||||
@related_user_resource(UserPlan)
|
||||
@show_if(features.BILLING)
|
||||
class OrganizationRhSkuSubscriptionField(ApiResource):
|
||||
"""
|
||||
@ -1007,6 +1045,7 @@ class OrganizationRhSkuSubscriptionField(ApiResource):
|
||||
"""
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname("removeSkuFromOrg")
|
||||
def delete(self, orgname, subscription_id):
|
||||
"""
|
||||
Remove sku from an org
|
||||
@ -1054,4 +1093,6 @@ class UserSkuList(ApiResource):
|
||||
else:
|
||||
subscription["assigned_to_org"] = None
|
||||
|
||||
subscription["metadata"] = get_plan_using_rh_sku(subscription["sku"])
|
||||
|
||||
return user_subscriptions
|
||||
|
@ -6047,7 +6047,7 @@ SECURITY_TESTS: List[
|
||||
OrganizationRhSku,
|
||||
"POST",
|
||||
{"orgname": "buynlarge"},
|
||||
{"subscription_id": 12345},
|
||||
{"subscriptions": [{"subscription_id": 12345}]},
|
||||
None,
|
||||
401,
|
||||
),
|
||||
@ -6059,6 +6059,14 @@ SECURITY_TESTS: List[
|
||||
None,
|
||||
401,
|
||||
),
|
||||
(
|
||||
OrganizationRhSkuBatchRemoval,
|
||||
"POST",
|
||||
{"orgname": "buynlarge"},
|
||||
{"subscriptions": [{"subscription_id": 12345}]},
|
||||
None,
|
||||
401,
|
||||
),
|
||||
(OrgAutoPrunePolicies, "GET", {"orgname": "buynlarge"}, None, None, 401),
|
||||
(OrgAutoPrunePolicies, "GET", {"orgname": "buynlarge"}, None, "devtable", 200),
|
||||
(OrgAutoPrunePolicies, "GET", {"orgname": "unknown"}, None, "devtable", 403),
|
||||
|
11
static/css/directives/ui/org-binding.css
Normal file
11
static/css/directives/ui/org-binding.css
Normal file
@ -0,0 +1,11 @@
|
||||
.org-binding-settings-element .btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.org-binding-settings-element .form-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.org-binding-settings-element td.add-remove-section {
|
||||
margin: 10px;
|
||||
}
|
@ -5,16 +5,16 @@
|
||||
<tr>
|
||||
<td>Current Plan:</td>
|
||||
<td>
|
||||
<div class="sub-usage" ng-if="subscription.usedPrivateRepos > currentPlan.privateRepos">
|
||||
<div class="sub-usage" ng-if="subscription.usedPrivateRepos > (currentPlan.privateRepos + currentMarketplace)">
|
||||
<i class="fa fa-exclamation-triangle red"></i> <strong>{{ subscription.usedPrivateRepos }}</strong> private repositories exceeds the amount allowed by your plan. Upgrade your plan to avoid service disruptions.
|
||||
</div>
|
||||
|
||||
<div class="sub-usage" ng-if="subscription.usedPrivateRepos == currentPlan.privateRepos">
|
||||
<div class="sub-usage" ng-if="subscription.usedPrivateRepos == (currentPlan.privateRepos + currentMarketplace)">
|
||||
<i class="fa fa-exclamation-triangle yellow"></i> <strong>{{ subscription.usedPrivateRepos }}</strong> private repositories is the maximum allowed by your plan. Upgrade your plan to create more private repositories.
|
||||
</div>
|
||||
|
||||
<a class="co-modify-link" ng-href="{{ getEntityPrefix() }}/billing">{{ currentPlan.privateRepos }} private repositories</a>
|
||||
<div class="help-text">Up to {{ currentPlan.privateRepos }} private repositories, unlimited public repositories</div>
|
||||
<div class="help-text">Up to {{ currentPlan.privateRepos + currentMarketplace}} private repositories, unlimited public repositories</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="currentCard">
|
||||
|
53
static/directives/org-binding.html
Normal file
53
static/directives/org-binding.html
Normal file
@ -0,0 +1,53 @@
|
||||
<div class="org-binding-settings-element" >
|
||||
<span><h3>Monthly Subscriptions From Red Hat Customer Portal</h3></span>
|
||||
<div class="cor-loader-inline" ng-show="marketplaceLoading"></div>
|
||||
<span ng-show="!organization && !marketplaceLoading">
|
||||
<div ng-repeat="(sku, subscriptions) in userMarketplaceSubscriptions">
|
||||
{{subscriptions.length}}x {{ sku }}
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<table ng-show="organization && !marketplaceLoading">
|
||||
<tr class="indented-row" ng-repeat="(sku, subscriptions) in orgMarketplaceSubscriptions">
|
||||
<td>
|
||||
{{ subscriptions.length }} x {{ sku }} attached to this org
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="indented-row">
|
||||
<td style="padding: 10px">
|
||||
<select class="form-control" ng-model="subscriptionBinding">
|
||||
<option ng-repeat="(sku, subscriptions) in availableSubscriptions" value="{{ subscriptions }}">
|
||||
{{subscriptions.length}} x {{sku}}
|
||||
</option>
|
||||
</select>
|
||||
<input class="form-control" type="number" min="1" max="{{subscriptions.length}}" ng-model="numSubscriptions" placeholder="Number of subscriptions">
|
||||
<a class="btn btn-primary" ng-click="bindSku(subscriptionBinding, numSubscriptions)">Attach subscriptions</a>
|
||||
</td>
|
||||
<td style="padding: 10px">
|
||||
<select class="form-control" ng-model="subscriptionRemovals">
|
||||
<option ng-repeat="(sku, orgSubscriptions) in orgMarketplaceSubscriptions" value="{{orgSubscriptions}}">
|
||||
{{sku}}
|
||||
</option>
|
||||
</select>
|
||||
<input class="form-control"
|
||||
type="number"
|
||||
min="1"
|
||||
max="{{JSON.parse(subscriptions).length}}"
|
||||
ng-model="numRemovals"
|
||||
placeholder="Number of subscriptions"
|
||||
>
|
||||
<a class="btn btn-default" ng-click="batchRemoveSku(subscriptionRemovals, numRemovals)">
|
||||
Remove subscriptions
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<div class="co-alert co-alert-success" ng-show="bindOrgSuccess">
|
||||
Successfully bound subscription to org
|
||||
</div>
|
||||
<div class="co-alert co-alert-success" ng-show="removeSkuSuccess">
|
||||
Successfully removed subscription from org
|
||||
</div>
|
||||
<tr>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
@ -41,15 +41,26 @@
|
||||
</div>
|
||||
|
||||
<!-- Chart -->
|
||||
<div class="usage-chart" total="subscribedPlan.privateRepos || 0"
|
||||
<div class="usage-chart"
|
||||
current="subscription.usedPrivateRepos || 0"
|
||||
limit="limit"
|
||||
total="subscribedPlan.privateRepos || 0"
|
||||
marketplace-total="marketplaceTotal"
|
||||
usage-title="Repository Usage"
|
||||
ng-show="!planLoading"></div>
|
||||
|
||||
<!-- Org Binding -->
|
||||
<div class="org-binding"
|
||||
ng-show="!planLoading"
|
||||
organization="organization"
|
||||
marketplace-total="marketplaceTotal"></div>
|
||||
|
||||
<hr></hr>
|
||||
|
||||
<!-- Plans Table -->
|
||||
<div class="visible-xs" style="margin-top: 10px"></div>
|
||||
|
||||
<h3>Monthly Subscriptions Purchased via Stripe</h3>
|
||||
<table class="table table-hover plans-list-table" ng-show="!planLoading">
|
||||
<thead>
|
||||
<td>Plan</td>
|
||||
|
@ -21,6 +21,7 @@ angular.module('quay').directive('billingManagementPanel', function () {
|
||||
$scope.changeReceiptsInfo = null;
|
||||
$scope.context = {};
|
||||
$scope.subscriptionStatus = 'loading';
|
||||
$scope.currentMarketplace = 0;
|
||||
|
||||
var setSubscription = function(sub) {
|
||||
$scope.subscription = sub;
|
||||
@ -44,6 +45,28 @@ angular.module('quay').directive('billingManagementPanel', function () {
|
||||
});
|
||||
};
|
||||
|
||||
var getMarketplace = function() {
|
||||
var total = 0;
|
||||
if ($scope.organization) {
|
||||
PlanService.listOrgMarketplaceSubscriptions($scope.organization.name, function(subscriptions){
|
||||
for (var i = 0; i < subscriptions.length; i++) {
|
||||
total += subscriptions[i]["metadata"]["privateRepos"];
|
||||
}
|
||||
$scope.currentMarketplace = total;
|
||||
})
|
||||
} else {
|
||||
PlanService.listUserMarketplaceSubscriptions(function(subscriptions){
|
||||
for (var i = 0; i < subscriptions.length; i++) {
|
||||
if(subscriptions[i]["assigned_to_org"] === null) {
|
||||
total += subscriptions[i]["metadata"]["privateRepos"];
|
||||
}
|
||||
}
|
||||
$scope.currentMarketplace = total;
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var update = function() {
|
||||
if (!$scope.isEnabled || !($scope.user || $scope.organization) || !Features.BILLING) {
|
||||
return;
|
||||
@ -59,6 +82,10 @@ angular.module('quay').directive('billingManagementPanel', function () {
|
||||
PlanService.getSubscription($scope.organization, setSubscription, function() {
|
||||
setSubscription({ 'plan': PlanService.getFreePlan() });
|
||||
});
|
||||
|
||||
if (Features.RH_MARKETPLACE) {
|
||||
getMarketplace();
|
||||
}
|
||||
};
|
||||
|
||||
// Listen to plan changes.
|
||||
|
155
static/js/directives/ui/org-binding.js
Normal file
155
static/js/directives/ui/org-binding.js
Normal file
@ -0,0 +1,155 @@
|
||||
angular.module('quay').directive('orgBinding', function() {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/org-binding.html',
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'marketplaceTotal': '=marketplaceTotal',
|
||||
'organization': '=organization',
|
||||
},
|
||||
controller: function($scope, $timeout, PlanService, ApiService) {
|
||||
$scope.userMarketplaceSubscriptions = {};
|
||||
$scope.orgMarketplaceSubscriptions = {};
|
||||
$scope.availableSubscriptions = {};
|
||||
$scope.marketplaceLoading = true;
|
||||
$scope.bindOrgSuccess = false;
|
||||
$scope.removeSkuSuccess = false;
|
||||
|
||||
var groupSubscriptionsBySku = function(subscriptions) {
|
||||
const grouped = {};
|
||||
|
||||
subscriptions.forEach(obj => {
|
||||
const { sku, ...rest } = obj;
|
||||
if(!grouped[sku]) {
|
||||
grouped[sku] = [];
|
||||
}
|
||||
grouped[sku].push(rest);
|
||||
});
|
||||
return grouped;
|
||||
}
|
||||
|
||||
var loadSubscriptions = function() {
|
||||
var total = 0;
|
||||
|
||||
if ($scope.organization) {
|
||||
PlanService.listOrgMarketplaceSubscriptions($scope.organization, function(marketplaceSubscriptions){
|
||||
// group the list of subscriptions by their sku field
|
||||
$scope.orgMarketplaceSubscriptions = groupSubscriptionsBySku(marketplaceSubscriptions);
|
||||
for (var i = 0; i < marketplaceSubscriptions.length; i++) {
|
||||
total += marketplaceSubscriptions[i]["metadata"]["privateRepos"];
|
||||
}
|
||||
$scope.marketplaceTotal = total;
|
||||
});
|
||||
}
|
||||
|
||||
PlanService.listUserMarketplaceSubscriptions(function(marketplaceSubscriptions){
|
||||
if(!marketplaceSubscriptions) {
|
||||
$scope.marketplaceLoading = false;
|
||||
return;
|
||||
}
|
||||
let notBound = [];
|
||||
$scope.userMarketplaceSubscriptions = groupSubscriptionsBySku(marketplaceSubscriptions);
|
||||
|
||||
for (var i = 0; i < marketplaceSubscriptions.length; i++) {
|
||||
if (marketplaceSubscriptions[i]["assigned_to_org"] === null) {
|
||||
if(!($scope.organization)){
|
||||
total += marketplaceSubscriptions[i]["metadata"]["privateRepos"];
|
||||
}
|
||||
notBound.push(marketplaceSubscriptions[i]);
|
||||
}
|
||||
}
|
||||
if(!($scope.organization)){
|
||||
$scope.marketplaceTotal = total;
|
||||
}
|
||||
$scope.availableSubscriptions = groupSubscriptionsBySku(notBound);
|
||||
$scope.marketplaceLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
var update = function() {
|
||||
$scope.marketplaceLoading = true;
|
||||
loadSubscriptions();
|
||||
}
|
||||
|
||||
$scope.bindSku = function(subscriptions, numSubscriptions) {
|
||||
let subscriptionArr = JSON.parse(subscriptions);
|
||||
if(numSubscriptions > subscriptionArr.length){
|
||||
displayError("number of subscriptions exceeds total amount");
|
||||
return;
|
||||
}
|
||||
$scope.marketplaceLoading = true;
|
||||
const requestData = {};
|
||||
requestData["subscriptions"] = [];
|
||||
for(var i = 0; i < numSubscriptions; ++i) {
|
||||
var subscriptionObject = {};
|
||||
var subscriptionId = subscriptionArr[i].id;
|
||||
subscriptionObject.subscription_id = subscriptionId;
|
||||
requestData["subscriptions"].push(subscriptionObject);
|
||||
}
|
||||
PlanService.bindSkuToOrg(requestData, $scope.organization, function(resp){
|
||||
if (resp === "Okay"){
|
||||
bindSkuSuccessMessage();
|
||||
}
|
||||
else {
|
||||
displayError(resp.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.batchRemoveSku = function(removals, numRemovals) {
|
||||
let removalArr = JSON.parse(removals);
|
||||
const requestData = {};
|
||||
requestData["subscriptions"] = [];
|
||||
for(var i = 0; i < numRemovals; ++i){
|
||||
var subscriptionObject = {};
|
||||
var subscriptionId = removalArr[i].subscription_id;
|
||||
subscriptionObject.subscription_id = subscriptionId;
|
||||
requestData["subscriptions"].push(subscriptionObject);
|
||||
}
|
||||
PlanService.batchRemoveSku(requestData, $scope.organization, function(resp){
|
||||
if (resp == "") {
|
||||
removeSkuSuccessMessage();
|
||||
}
|
||||
else {
|
||||
displayError(resp.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var displayError = function (message = "Could not update org") {
|
||||
let errorDisplay = ApiService.errorDisplay(message, () => {
|
||||
});
|
||||
return errorDisplay;
|
||||
}
|
||||
|
||||
var bindSkuSuccessMessage = function () {
|
||||
$timeout(function () {
|
||||
$scope.bindOrgSuccess = true;
|
||||
}, 1);
|
||||
$timeout(function () {
|
||||
$scope.bindOrgSuccess = false;
|
||||
}, 5000)
|
||||
};
|
||||
|
||||
var removeSkuSuccessMessage = function () {
|
||||
$timeout(function () {
|
||||
$scope.removeSkuSuccess = true;
|
||||
}, 1);
|
||||
$timeout(function () {
|
||||
$scope.removeSkuSuccess = false;
|
||||
}, 5000)
|
||||
};
|
||||
|
||||
loadSubscriptions();
|
||||
|
||||
$scope.$watch('bindOrgSuccess', function(){
|
||||
if($scope.bindOrgSuccess === true) { update(); }
|
||||
});
|
||||
$scope.$watch('removeSkuSuccess', function(){
|
||||
if($scope.removeSkuSuccess === true) { update(); }
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
@ -20,6 +20,8 @@ angular.module('quay').directive('planManager', function () {
|
||||
controller: function($scope, $element, PlanService, ApiService) {
|
||||
$scope.isExistingCustomer = false;
|
||||
|
||||
$scope.marketplaceTotal = 0;
|
||||
|
||||
$scope.parseDate = function(timestamp) {
|
||||
return new Date(timestamp * 1000);
|
||||
};
|
||||
|
@ -15,23 +15,29 @@ angular.module('quay').directive('usageChart', function () {
|
||||
scope: {
|
||||
'current': '=current',
|
||||
'total': '=total',
|
||||
'marketplaceTotal': '=marketplaceTotal',
|
||||
'limit': '=limit',
|
||||
'usageTitle': '@usageTitle'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
if($scope.subscribedPlan !== undefined){
|
||||
$scope.total = $scope.subscribedPlan.privateRepos || 0;
|
||||
}
|
||||
else {
|
||||
$scope.total = 0;
|
||||
}
|
||||
$scope.limit = "";
|
||||
|
||||
var chart = null;
|
||||
|
||||
var update = function() {
|
||||
if ($scope.current == null || $scope.total == null) { return; }
|
||||
if (!chart) {
|
||||
chart = new UsageChart();
|
||||
chart.draw('usage-chart-element');
|
||||
}
|
||||
|
||||
var current = $scope.current || 0;
|
||||
var total = $scope.total || 0;
|
||||
var total = $scope.total + $scope.marketplaceTotal;
|
||||
if (current > total) {
|
||||
$scope.limit = 'over';
|
||||
} else if (current == total) {
|
||||
@ -42,11 +48,13 @@ angular.module('quay').directive('usageChart', function () {
|
||||
$scope.limit = 'none';
|
||||
}
|
||||
|
||||
chart.update($scope.current, $scope.total);
|
||||
var finalAmount = $scope.total + $scope.marketplaceTotal;
|
||||
chart.update($scope.current, finalAmount);
|
||||
};
|
||||
|
||||
$scope.$watch('current', update);
|
||||
$scope.$watch('total', update);
|
||||
$scope.$watch('marketplaceTotal', update);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
|
@ -306,5 +306,46 @@ function(KeyService, UserService, CookieService, ApiService, Features, Config, $
|
||||
);
|
||||
};
|
||||
|
||||
planService.listUserMarketplaceSubscriptions = function(callback) {
|
||||
if (!Features.BILLING || !Features.RH_MARKETPLACE) { return; }
|
||||
|
||||
var errorHandler = function(resp) {
|
||||
if (resp.status == 404) {
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
|
||||
ApiService.getUserMarketplaceSubscriptions().then(callback, errorHandler);
|
||||
};
|
||||
|
||||
planService.listOrgMarketplaceSubscriptions = function(orgname, callback) {
|
||||
if (!Features.BILLING || !Features.RH_MARKETPLACE) { return; }
|
||||
var params = {
|
||||
'orgname': orgname
|
||||
}
|
||||
ApiService.listOrgSkus(null, params).then(function(resp) {
|
||||
callback(resp);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
planService.bindSkuToOrg = function(subscriptions, orgname, callback) {
|
||||
if(!Features.BILLING || !Features.RH_MARKETPLACE) { return; }
|
||||
var params = {
|
||||
'orgname': orgname
|
||||
};
|
||||
ApiService.bindSkuToOrg(subscriptions, params).then(function(resp) {
|
||||
callback(resp);
|
||||
});
|
||||
};
|
||||
|
||||
planService.batchRemoveSku = function(subscriptions, orgname, callback) {
|
||||
if(!Features.BILLING || !Features.RH_MARKETPLACE) { return; }
|
||||
var params = {
|
||||
'orgname': orgname
|
||||
};
|
||||
ApiService.batchRemoveSku(subscriptions, params).then(callback);
|
||||
};
|
||||
|
||||
return planService;
|
||||
}]);
|
||||
|
@ -37,9 +37,11 @@ from endpoints.api.billing import (
|
||||
OrganizationCard,
|
||||
OrganizationPlan,
|
||||
OrganizationRhSku,
|
||||
OrganizationRhSkuBatchRemoval,
|
||||
OrganizationRhSkuSubscriptionField,
|
||||
UserCard,
|
||||
UserPlan,
|
||||
UserSkuList,
|
||||
)
|
||||
from endpoints.api.build import (
|
||||
RepositoryBuildList,
|
||||
@ -5076,7 +5078,7 @@ class TestOrganizationRhSku(ApiTestCase):
|
||||
self.postResponse(
|
||||
resource_name=OrganizationRhSku,
|
||||
params=dict(orgname=SUBSCRIPTION_ORG),
|
||||
data={"subscription_id": 12345678},
|
||||
data={"subscriptions": [{"subscription_id": 12345678}]},
|
||||
expected_code=201,
|
||||
)
|
||||
json = self.getJsonResponse(
|
||||
@ -5093,7 +5095,7 @@ class TestOrganizationRhSku(ApiTestCase):
|
||||
self.postResponse(
|
||||
resource_name=OrganizationRhSku,
|
||||
params=dict(orgname=SUBSCRIPTION_ORG),
|
||||
data={"subscription_id": 12345678},
|
||||
data={"subscriptions": [{"subscription_id": 12345678}]},
|
||||
expected_code=400,
|
||||
)
|
||||
|
||||
@ -5103,7 +5105,7 @@ class TestOrganizationRhSku(ApiTestCase):
|
||||
self.postResponse(
|
||||
resource_name=OrganizationRhSku,
|
||||
params=dict(orgname=SUBSCRIPTION_ORG),
|
||||
data={"subscription_id": 11111111},
|
||||
data={"subscriptions": [{"subscription_id": 11111}]},
|
||||
expected_code=401,
|
||||
)
|
||||
|
||||
@ -5112,7 +5114,7 @@ class TestOrganizationRhSku(ApiTestCase):
|
||||
self.postResponse(
|
||||
resource_name=OrganizationRhSku,
|
||||
params=dict(orgname=SUBSCRIPTION_ORG),
|
||||
data={"subscription_id": 12345678},
|
||||
data={"subscriptions": [{"subscription_id": 12345678}]},
|
||||
expected_code=201,
|
||||
)
|
||||
self.deleteResponse(
|
||||
@ -5126,6 +5128,49 @@ class TestOrganizationRhSku(ApiTestCase):
|
||||
)
|
||||
self.assertEqual(len(json), 0)
|
||||
|
||||
def test_sku_stacking(self):
|
||||
# multiples of same sku
|
||||
self.login(SUBSCRIPTION_USER)
|
||||
self.postResponse(
|
||||
resource_name=OrganizationRhSku,
|
||||
params=dict(orgname=SUBSCRIPTION_ORG),
|
||||
data={"subscriptions": [{"subscription_id": 12345678}, {"subscription_id": 11223344}]},
|
||||
expected_code=201,
|
||||
)
|
||||
json = self.getJsonResponse(
|
||||
resource_name=OrganizationRhSku,
|
||||
params=dict(orgname=SUBSCRIPTION_ORG),
|
||||
)
|
||||
self.assertEqual(len(json), 2)
|
||||
json = self.getJsonResponse(OrgPrivateRepositories, params=dict(orgname=SUBSCRIPTION_ORG))
|
||||
self.assertEqual(True, json["privateAllowed"])
|
||||
|
||||
def test_batch_sku_remove(self):
|
||||
self.login(SUBSCRIPTION_USER)
|
||||
self.postResponse(
|
||||
resource_name=OrganizationRhSku,
|
||||
params=dict(orgname=SUBSCRIPTION_ORG),
|
||||
data={"subscriptions": [{"subscription_id": 12345678}, {"subscription_id": 11223344}]},
|
||||
expected_code=201,
|
||||
)
|
||||
self.postResponse(
|
||||
resource_name=OrganizationRhSkuBatchRemoval,
|
||||
params=dict(orgname=SUBSCRIPTION_ORG),
|
||||
data={"subscriptions": [{"subscription_id": 12345678}, {"subscription_id": 11223344}]},
|
||||
expected_code=204,
|
||||
)
|
||||
json = self.getJsonResponse(
|
||||
resource_name=OrganizationRhSku, params=dict(orgname=SUBSCRIPTION_ORG)
|
||||
)
|
||||
self.assertEqual(len(json), 0)
|
||||
|
||||
|
||||
class TestUserSku(ApiTestCase):
|
||||
def test_get_user_skus(self):
|
||||
self.login(SUBSCRIPTION_USER)
|
||||
json = self.getJsonResponse(UserSkuList)
|
||||
self.assertEqual(len(json), 2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
@ -39,6 +39,8 @@ def test_create_for_stripe_user(initialized_db):
|
||||
with patch.object(marketplace_subscriptions, "create_entitlement") as mock:
|
||||
worker._perform_reconciliation(marketplace_users, marketplace_subscriptions)
|
||||
|
||||
# expect that entitlment is created with account number
|
||||
mock.assert_called_with(11111, "FakeSKU")
|
||||
# expect that entitlment is created with customer id number
|
||||
mock.assert_called_with(model.entitlements.get_web_customer_id(test_user.id), "FakeSKU")
|
||||
|
||||
|
Reference in New Issue
Block a user