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 random
|
||||||
import string
|
import string
|
||||||
|
import sys
|
||||||
from calendar import timegm
|
from calendar import timegm
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
@ -292,6 +293,7 @@ PLANS = [
|
|||||||
"price": 45000,
|
"price": 45000,
|
||||||
"privateRepos": 250,
|
"privateRepos": 250,
|
||||||
"rh_sku": "MW00589MO",
|
"rh_sku": "MW00589MO",
|
||||||
|
"billing_enabled": False,
|
||||||
"stripeId": "bus-xlarge-2018",
|
"stripeId": "bus-xlarge-2018",
|
||||||
"audience": "For extra large businesses",
|
"audience": "For extra large businesses",
|
||||||
"bus_features": True,
|
"bus_features": True,
|
||||||
@ -305,6 +307,7 @@ PLANS = [
|
|||||||
"price": 85000,
|
"price": 85000,
|
||||||
"privateRepos": 500,
|
"privateRepos": 500,
|
||||||
"rh_sku": "MW00590MO",
|
"rh_sku": "MW00590MO",
|
||||||
|
"billing_enabled": False,
|
||||||
"stripeId": "bus-500-2018",
|
"stripeId": "bus-500-2018",
|
||||||
"audience": "For huge business",
|
"audience": "For huge business",
|
||||||
"bus_features": True,
|
"bus_features": True,
|
||||||
@ -354,13 +357,21 @@ PLANS = [
|
|||||||
"plans_page_hidden": False,
|
"plans_page_hidden": False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "subscriptionwatch",
|
"title": "premium",
|
||||||
"privateRepos": 100,
|
"privateRepos": 100,
|
||||||
"stripeId": "not_a_stripe_plan",
|
"stripeId": "not_a_stripe_plan",
|
||||||
"rh_sku": "MW02701",
|
"rh_sku": "MW02701",
|
||||||
"sku_billing": True,
|
"sku_billing": True,
|
||||||
"plans_page_hidden": 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 = [
|
RH_SKUS = [
|
||||||
@ -389,6 +400,8 @@ def get_plan_using_rh_sku(sku):
|
|||||||
"""
|
"""
|
||||||
Returns the plan with given sku or None if none.
|
Returns the plan with given sku or None if none.
|
||||||
"""
|
"""
|
||||||
|
if sku is None:
|
||||||
|
return None
|
||||||
for plan in PLANS:
|
for plan in PLANS:
|
||||||
if plan.get("rh_sku") == sku:
|
if plan.get("rh_sku") == sku:
|
||||||
return plan
|
return plan
|
||||||
|
@ -955,9 +955,11 @@ class OrganizationRhSku(ApiResource):
|
|||||||
if query:
|
if query:
|
||||||
subscriptions = list(query.dicts())
|
subscriptions = list(query.dicts())
|
||||||
for subscription in subscriptions:
|
for subscription in subscriptions:
|
||||||
subscription["sku"] = marketplace_subscriptions.get_subscription_sku(
|
subscription_sku = marketplace_subscriptions.get_subscription_sku(
|
||||||
subscription["subscription_id"]
|
subscription["subscription_id"]
|
||||||
)
|
)
|
||||||
|
subscription["sku"] = subscription_sku
|
||||||
|
subscription["metadata"] = get_plan_using_rh_sku(subscription_sku)
|
||||||
return subscriptions
|
return subscriptions
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
@ -971,35 +973,71 @@ class OrganizationRhSku(ApiResource):
|
|||||||
"""
|
"""
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
request_data = request.get_json()
|
request_data = request.get_json()
|
||||||
subscription_id = request_data["subscription_id"]
|
organization = model.organization.get_organization(orgname)
|
||||||
|
subscriptions = request_data["subscriptions"]
|
||||||
if permission.can():
|
if permission.can():
|
||||||
organization = model.organization.get_organization(orgname)
|
for subscription in subscriptions:
|
||||||
user = get_authenticated_user()
|
subscription_id = subscription.get("subscription_id")
|
||||||
account_number = marketplace_users.get_account_number(user)
|
if subscription_id is None:
|
||||||
subscriptions = marketplace_subscriptions.get_list_of_subscriptions(account_number)
|
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:
|
if subscriptions is None:
|
||||||
abort(401, message="no valid subscriptions present")
|
abort(401, message="no valid subscriptions present")
|
||||||
|
|
||||||
user_subscription_ids = [int(subscription["id"]) for subscription in subscriptions]
|
user_subscription_ids = [int(subscription["id"]) for subscription in subscriptions]
|
||||||
if int(subscription_id) in user_subscription_ids:
|
if int(subscription_id) in user_subscription_ids:
|
||||||
try:
|
try:
|
||||||
model.organization_skus.bind_subscription_to_org(
|
model.organization_skus.bind_subscription_to_org(
|
||||||
user_id=user.id, subscription_id=subscription_id, org_id=organization.id
|
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)
|
abort(401)
|
||||||
|
|
||||||
|
|
||||||
@resource("/v1/organization/<orgname>/marketplace/<subscription_id>")
|
@resource("/v1/organization/<orgname>/marketplace/<subscription_id>")
|
||||||
@path_param("orgname", "The name of the organization")
|
@path_param("orgname", "The name of the organization")
|
||||||
@path_param("subscription_id", "Marketplace subscription id")
|
@path_param("subscription_id", "Marketplace subscription id")
|
||||||
@related_user_resource(UserPlan)
|
|
||||||
@show_if(features.BILLING)
|
@show_if(features.BILLING)
|
||||||
class OrganizationRhSkuSubscriptionField(ApiResource):
|
class OrganizationRhSkuSubscriptionField(ApiResource):
|
||||||
"""
|
"""
|
||||||
@ -1007,6 +1045,7 @@ class OrganizationRhSkuSubscriptionField(ApiResource):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@require_scope(scopes.ORG_ADMIN)
|
@require_scope(scopes.ORG_ADMIN)
|
||||||
|
@nickname("removeSkuFromOrg")
|
||||||
def delete(self, orgname, subscription_id):
|
def delete(self, orgname, subscription_id):
|
||||||
"""
|
"""
|
||||||
Remove sku from an org
|
Remove sku from an org
|
||||||
@ -1054,4 +1093,6 @@ class UserSkuList(ApiResource):
|
|||||||
else:
|
else:
|
||||||
subscription["assigned_to_org"] = None
|
subscription["assigned_to_org"] = None
|
||||||
|
|
||||||
|
subscription["metadata"] = get_plan_using_rh_sku(subscription["sku"])
|
||||||
|
|
||||||
return user_subscriptions
|
return user_subscriptions
|
||||||
|
@ -6047,7 +6047,7 @@ SECURITY_TESTS: List[
|
|||||||
OrganizationRhSku,
|
OrganizationRhSku,
|
||||||
"POST",
|
"POST",
|
||||||
{"orgname": "buynlarge"},
|
{"orgname": "buynlarge"},
|
||||||
{"subscription_id": 12345},
|
{"subscriptions": [{"subscription_id": 12345}]},
|
||||||
None,
|
None,
|
||||||
401,
|
401,
|
||||||
),
|
),
|
||||||
@ -6059,6 +6059,14 @@ SECURITY_TESTS: List[
|
|||||||
None,
|
None,
|
||||||
401,
|
401,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
OrganizationRhSkuBatchRemoval,
|
||||||
|
"POST",
|
||||||
|
{"orgname": "buynlarge"},
|
||||||
|
{"subscriptions": [{"subscription_id": 12345}]},
|
||||||
|
None,
|
||||||
|
401,
|
||||||
|
),
|
||||||
(OrgAutoPrunePolicies, "GET", {"orgname": "buynlarge"}, None, None, 401),
|
(OrgAutoPrunePolicies, "GET", {"orgname": "buynlarge"}, None, None, 401),
|
||||||
(OrgAutoPrunePolicies, "GET", {"orgname": "buynlarge"}, None, "devtable", 200),
|
(OrgAutoPrunePolicies, "GET", {"orgname": "buynlarge"}, None, "devtable", 200),
|
||||||
(OrgAutoPrunePolicies, "GET", {"orgname": "unknown"}, None, "devtable", 403),
|
(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>
|
<tr>
|
||||||
<td>Current Plan:</td>
|
<td>Current Plan:</td>
|
||||||
<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.
|
<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>
|
||||||
|
|
||||||
<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.
|
<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>
|
</div>
|
||||||
|
|
||||||
<a class="co-modify-link" ng-href="{{ getEntityPrefix() }}/billing">{{ currentPlan.privateRepos }} private repositories</a>
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-show="currentCard">
|
<tr ng-show="currentCard">
|
||||||
@ -77,4 +77,4 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- Chart -->
|
<!-- Chart -->
|
||||||
<div class="usage-chart" total="subscribedPlan.privateRepos || 0"
|
<div class="usage-chart"
|
||||||
current="subscription.usedPrivateRepos || 0"
|
current="subscription.usedPrivateRepos || 0"
|
||||||
limit="limit"
|
limit="limit"
|
||||||
|
total="subscribedPlan.privateRepos || 0"
|
||||||
|
marketplace-total="marketplaceTotal"
|
||||||
usage-title="Repository Usage"
|
usage-title="Repository Usage"
|
||||||
ng-show="!planLoading"></div>
|
ng-show="!planLoading"></div>
|
||||||
|
|
||||||
|
<!-- Org Binding -->
|
||||||
|
<div class="org-binding"
|
||||||
|
ng-show="!planLoading"
|
||||||
|
organization="organization"
|
||||||
|
marketplace-total="marketplaceTotal"></div>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
|
||||||
<!-- Plans Table -->
|
<!-- Plans Table -->
|
||||||
<div class="visible-xs" style="margin-top: 10px"></div>
|
<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">
|
<table class="table table-hover plans-list-table" ng-show="!planLoading">
|
||||||
<thead>
|
<thead>
|
||||||
<td>Plan</td>
|
<td>Plan</td>
|
||||||
|
@ -21,6 +21,7 @@ angular.module('quay').directive('billingManagementPanel', function () {
|
|||||||
$scope.changeReceiptsInfo = null;
|
$scope.changeReceiptsInfo = null;
|
||||||
$scope.context = {};
|
$scope.context = {};
|
||||||
$scope.subscriptionStatus = 'loading';
|
$scope.subscriptionStatus = 'loading';
|
||||||
|
$scope.currentMarketplace = 0;
|
||||||
|
|
||||||
var setSubscription = function(sub) {
|
var setSubscription = function(sub) {
|
||||||
$scope.subscription = 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() {
|
var update = function() {
|
||||||
if (!$scope.isEnabled || !($scope.user || $scope.organization) || !Features.BILLING) {
|
if (!$scope.isEnabled || !($scope.user || $scope.organization) || !Features.BILLING) {
|
||||||
return;
|
return;
|
||||||
@ -59,6 +82,10 @@ angular.module('quay').directive('billingManagementPanel', function () {
|
|||||||
PlanService.getSubscription($scope.organization, setSubscription, function() {
|
PlanService.getSubscription($scope.organization, setSubscription, function() {
|
||||||
setSubscription({ 'plan': PlanService.getFreePlan() });
|
setSubscription({ 'plan': PlanService.getFreePlan() });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (Features.RH_MARKETPLACE) {
|
||||||
|
getMarketplace();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listen to plan changes.
|
// Listen to plan changes.
|
||||||
@ -141,4 +168,4 @@ angular.module('quay').directive('billingManagementPanel', function () {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
});
|
});
|
||||||
|
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) {
|
controller: function($scope, $element, PlanService, ApiService) {
|
||||||
$scope.isExistingCustomer = false;
|
$scope.isExistingCustomer = false;
|
||||||
|
|
||||||
|
$scope.marketplaceTotal = 0;
|
||||||
|
|
||||||
$scope.parseDate = function(timestamp) {
|
$scope.parseDate = function(timestamp) {
|
||||||
return new Date(timestamp * 1000);
|
return new Date(timestamp * 1000);
|
||||||
};
|
};
|
||||||
|
@ -15,23 +15,29 @@ angular.module('quay').directive('usageChart', function () {
|
|||||||
scope: {
|
scope: {
|
||||||
'current': '=current',
|
'current': '=current',
|
||||||
'total': '=total',
|
'total': '=total',
|
||||||
|
'marketplaceTotal': '=marketplaceTotal',
|
||||||
'limit': '=limit',
|
'limit': '=limit',
|
||||||
'usageTitle': '@usageTitle'
|
'usageTitle': '@usageTitle'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element) {
|
controller: function($scope, $element) {
|
||||||
|
if($scope.subscribedPlan !== undefined){
|
||||||
|
$scope.total = $scope.subscribedPlan.privateRepos || 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.total = 0;
|
||||||
|
}
|
||||||
$scope.limit = "";
|
$scope.limit = "";
|
||||||
|
|
||||||
var chart = null;
|
var chart = null;
|
||||||
|
|
||||||
var update = function() {
|
var update = function() {
|
||||||
if ($scope.current == null || $scope.total == null) { return; }
|
|
||||||
if (!chart) {
|
if (!chart) {
|
||||||
chart = new UsageChart();
|
chart = new UsageChart();
|
||||||
chart.draw('usage-chart-element');
|
chart.draw('usage-chart-element');
|
||||||
}
|
}
|
||||||
|
|
||||||
var current = $scope.current || 0;
|
var current = $scope.current || 0;
|
||||||
var total = $scope.total || 0;
|
var total = $scope.total + $scope.marketplaceTotal;
|
||||||
if (current > total) {
|
if (current > total) {
|
||||||
$scope.limit = 'over';
|
$scope.limit = 'over';
|
||||||
} else if (current == total) {
|
} else if (current == total) {
|
||||||
@ -42,11 +48,13 @@ angular.module('quay').directive('usageChart', function () {
|
|||||||
$scope.limit = 'none';
|
$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('current', update);
|
||||||
$scope.$watch('total', update);
|
$scope.$watch('total', update);
|
||||||
|
$scope.$watch('marketplaceTotal', update);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directiveDefinitionObject;
|
return directiveDefinitionObject;
|
||||||
|
@ -198,11 +198,11 @@ function(KeyService, UserService, CookieService, ApiService, Features, Config, $
|
|||||||
|
|
||||||
planService.updateSubscription = function($scope, orgname, planId, success, failure) {
|
planService.updateSubscription = function($scope, orgname, planId, success, failure) {
|
||||||
if (!Features.BILLING) { return; }
|
if (!Features.BILLING) { return; }
|
||||||
|
|
||||||
var subscriptionDetails = {
|
var subscriptionDetails = {
|
||||||
plan: planId,
|
plan: planId,
|
||||||
};
|
};
|
||||||
|
|
||||||
planService.getPlan(planId, function(plan) {
|
planService.getPlan(planId, function(plan) {
|
||||||
ApiService.updateSubscription(orgname, subscriptionDetails).then(
|
ApiService.updateSubscription(orgname, subscriptionDetails).then(
|
||||||
function(resp) {
|
function(resp) {
|
||||||
@ -214,7 +214,7 @@ function(KeyService, UserService, CookieService, ApiService, Features, Config, $
|
|||||||
failure
|
failure
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
planService.getCardInfo = function(orgname, callback) {
|
planService.getCardInfo = function(orgname, callback) {
|
||||||
if (!Features.BILLING) { return; }
|
if (!Features.BILLING) { return; }
|
||||||
@ -297,7 +297,7 @@ function(KeyService, UserService, CookieService, ApiService, Features, Config, $
|
|||||||
callbacks['success'](resp)
|
callbacks['success'](resp)
|
||||||
document.location = resp.url;
|
document.location = resp.url;
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
},
|
},
|
||||||
function(resp) {
|
function(resp) {
|
||||||
planService.handleCardError(resp);
|
planService.handleCardError(resp);
|
||||||
@ -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;
|
return planService;
|
||||||
}]);
|
}]);
|
||||||
|
@ -37,9 +37,11 @@ from endpoints.api.billing import (
|
|||||||
OrganizationCard,
|
OrganizationCard,
|
||||||
OrganizationPlan,
|
OrganizationPlan,
|
||||||
OrganizationRhSku,
|
OrganizationRhSku,
|
||||||
|
OrganizationRhSkuBatchRemoval,
|
||||||
OrganizationRhSkuSubscriptionField,
|
OrganizationRhSkuSubscriptionField,
|
||||||
UserCard,
|
UserCard,
|
||||||
UserPlan,
|
UserPlan,
|
||||||
|
UserSkuList,
|
||||||
)
|
)
|
||||||
from endpoints.api.build import (
|
from endpoints.api.build import (
|
||||||
RepositoryBuildList,
|
RepositoryBuildList,
|
||||||
@ -5076,7 +5078,7 @@ class TestOrganizationRhSku(ApiTestCase):
|
|||||||
self.postResponse(
|
self.postResponse(
|
||||||
resource_name=OrganizationRhSku,
|
resource_name=OrganizationRhSku,
|
||||||
params=dict(orgname=SUBSCRIPTION_ORG),
|
params=dict(orgname=SUBSCRIPTION_ORG),
|
||||||
data={"subscription_id": 12345678},
|
data={"subscriptions": [{"subscription_id": 12345678}]},
|
||||||
expected_code=201,
|
expected_code=201,
|
||||||
)
|
)
|
||||||
json = self.getJsonResponse(
|
json = self.getJsonResponse(
|
||||||
@ -5093,7 +5095,7 @@ class TestOrganizationRhSku(ApiTestCase):
|
|||||||
self.postResponse(
|
self.postResponse(
|
||||||
resource_name=OrganizationRhSku,
|
resource_name=OrganizationRhSku,
|
||||||
params=dict(orgname=SUBSCRIPTION_ORG),
|
params=dict(orgname=SUBSCRIPTION_ORG),
|
||||||
data={"subscription_id": 12345678},
|
data={"subscriptions": [{"subscription_id": 12345678}]},
|
||||||
expected_code=400,
|
expected_code=400,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -5103,7 +5105,7 @@ class TestOrganizationRhSku(ApiTestCase):
|
|||||||
self.postResponse(
|
self.postResponse(
|
||||||
resource_name=OrganizationRhSku,
|
resource_name=OrganizationRhSku,
|
||||||
params=dict(orgname=SUBSCRIPTION_ORG),
|
params=dict(orgname=SUBSCRIPTION_ORG),
|
||||||
data={"subscription_id": 11111111},
|
data={"subscriptions": [{"subscription_id": 11111}]},
|
||||||
expected_code=401,
|
expected_code=401,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -5112,7 +5114,7 @@ class TestOrganizationRhSku(ApiTestCase):
|
|||||||
self.postResponse(
|
self.postResponse(
|
||||||
resource_name=OrganizationRhSku,
|
resource_name=OrganizationRhSku,
|
||||||
params=dict(orgname=SUBSCRIPTION_ORG),
|
params=dict(orgname=SUBSCRIPTION_ORG),
|
||||||
data={"subscription_id": 12345678},
|
data={"subscriptions": [{"subscription_id": 12345678}]},
|
||||||
expected_code=201,
|
expected_code=201,
|
||||||
)
|
)
|
||||||
self.deleteResponse(
|
self.deleteResponse(
|
||||||
@ -5126,6 +5128,49 @@ class TestOrganizationRhSku(ApiTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(len(json), 0)
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -39,6 +39,8 @@ def test_create_for_stripe_user(initialized_db):
|
|||||||
with patch.object(marketplace_subscriptions, "create_entitlement") as mock:
|
with patch.object(marketplace_subscriptions, "create_entitlement") as mock:
|
||||||
worker._perform_reconciliation(marketplace_users, marketplace_subscriptions)
|
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
|
# expect that entitlment is created with customer id number
|
||||||
mock.assert_called_with(model.entitlements.get_web_customer_id(test_user.id), "FakeSKU")
|
mock.assert_called_with(model.entitlements.get_web_customer_id(test_user.id), "FakeSKU")
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user