From 2a4ac0930608f6d64bbf55b595736da2518a4bea Mon Sep 17 00:00:00 2001 From: Marcus Kok <47163063+Marcusk19@users.noreply.github.com> Date: Thu, 11 Jan 2024 11:48:38 -0500 Subject: [PATCH] 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 --- data/billing.py | 15 +- endpoints/api/billing.py | 81 ++++++--- endpoints/api/test/test_security.py | 10 +- static/css/directives/ui/org-binding.css | 11 ++ .../directives/billing-management-panel.html | 8 +- static/directives/org-binding.html | 53 ++++++ static/directives/plan-manager.html | 13 +- .../directives/ui/billing-management-panel.js | 29 +++- static/js/directives/ui/org-binding.js | 155 ++++++++++++++++++ static/js/directives/ui/plan-manager.js | 2 + static/js/directives/ui/usage-chart.js | 14 +- static/js/services/plan-service.js | 49 +++++- test/test_api_usage.py | 53 +++++- workers/test/test_reconciliationworker.py | 2 + 14 files changed, 456 insertions(+), 39 deletions(-) create mode 100644 static/css/directives/ui/org-binding.css create mode 100644 static/directives/org-binding.html create mode 100644 static/js/directives/ui/org-binding.js diff --git a/data/billing.py b/data/billing.py index bac9a436a0..0c31e28214 100644 --- a/data/billing.py +++ b/data/billing.py @@ -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 @@ "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 @@ "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_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 diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py index 056da32c7b..012cf87add 100644 --- a/endpoints/api/billing.py +++ b/endpoints/api/billing.py @@ -955,9 +955,11 @@ def get(self, orgname): 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 @@ def post(self, orgname): """ 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") + + 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}", + ) - if subscriptions is None: - abort(401, message="no valid subscriptions present") + return "Okay", 201 - 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 - ) - 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}") + abort(401) + +@resource("/v1/organization//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//marketplace/") @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 @@ def get(self): else: subscription["assigned_to_org"] = None + subscription["metadata"] = get_plan_using_rh_sku(subscription["sku"]) + return user_subscriptions diff --git a/endpoints/api/test/test_security.py b/endpoints/api/test/test_security.py index 3d5a822e29..ecbb43eef7 100644 --- a/endpoints/api/test/test_security.py +++ b/endpoints/api/test/test_security.py @@ -6047,7 +6047,7 @@ OrganizationRhSku, "POST", {"orgname": "buynlarge"}, - {"subscription_id": 12345}, + {"subscriptions": [{"subscription_id": 12345}]}, None, 401, ), @@ -6059,6 +6059,14 @@ 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), diff --git a/static/css/directives/ui/org-binding.css b/static/css/directives/ui/org-binding.css new file mode 100644 index 0000000000..eab2ffe4c4 --- /dev/null +++ b/static/css/directives/ui/org-binding.css @@ -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; +} diff --git a/static/directives/billing-management-panel.html b/static/directives/billing-management-panel.html index 3049d8d4da..a4d3b6cf62 100644 --- a/static/directives/billing-management-panel.html +++ b/static/directives/billing-management-panel.html @@ -5,16 +5,16 @@ Current Plan: -
+
{{ subscription.usedPrivateRepos }} private repositories exceeds the amount allowed by your plan. Upgrade your plan to avoid service disruptions.
-
+
{{ subscription.usedPrivateRepos }} private repositories is the maximum allowed by your plan. Upgrade your plan to create more private repositories.
{{ currentPlan.privateRepos }} private repositories -
Up to {{ currentPlan.privateRepos }} private repositories, unlimited public repositories
+
Up to {{ currentPlan.privateRepos + currentMarketplace}} private repositories, unlimited public repositories
@@ -77,4 +77,4 @@
-
\ No newline at end of file + diff --git a/static/directives/org-binding.html b/static/directives/org-binding.html new file mode 100644 index 0000000000..f8363935a5 --- /dev/null +++ b/static/directives/org-binding.html @@ -0,0 +1,53 @@ +
+

Monthly Subscriptions From Red Hat Customer Portal

+
+ +
+ {{subscriptions.length}}x {{ sku }} +
+
+ + + + + + + + + +
+ Successfully bound subscription to org +
+
+ Successfully removed subscription from org +
+ + +
+ {{ subscriptions.length }} x {{ sku }} attached to this org +
+ + + Attach subscriptions + + + + + Remove subscriptions + +
+
diff --git a/static/directives/plan-manager.html b/static/directives/plan-manager.html index f9125ead28..1e6d3c212a 100644 --- a/static/directives/plan-manager.html +++ b/static/directives/plan-manager.html @@ -41,15 +41,26 @@ -
+ +
+ +
+
+

Monthly Subscriptions Purchased via Stripe

diff --git a/static/js/directives/ui/billing-management-panel.js b/static/js/directives/ui/billing-management-panel.js index d31e52001b..d368f6c3c4 100644 --- a/static/js/directives/ui/billing-management-panel.js +++ b/static/js/directives/ui/billing-management-panel.js @@ -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. @@ -141,4 +168,4 @@ angular.module('quay').directive('billingManagementPanel', function () { } }; return directiveDefinitionObject; -}); \ No newline at end of file +}); diff --git a/static/js/directives/ui/org-binding.js b/static/js/directives/ui/org-binding.js new file mode 100644 index 0000000000..2ec2ed8c9f --- /dev/null +++ b/static/js/directives/ui/org-binding.js @@ -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; +}); diff --git a/static/js/directives/ui/plan-manager.js b/static/js/directives/ui/plan-manager.js index 4375fe63ce..6642506227 100644 --- a/static/js/directives/ui/plan-manager.js +++ b/static/js/directives/ui/plan-manager.js @@ -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); }; diff --git a/static/js/directives/ui/usage-chart.js b/static/js/directives/ui/usage-chart.js index 3a65d4c531..e0a4e62163 100644 --- a/static/js/directives/ui/usage-chart.js +++ b/static/js/directives/ui/usage-chart.js @@ -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; diff --git a/static/js/services/plan-service.js b/static/js/services/plan-service.js index 6f77e0a106..3938b39020 100644 --- a/static/js/services/plan-service.js +++ b/static/js/services/plan-service.js @@ -198,11 +198,11 @@ function(KeyService, UserService, CookieService, ApiService, Features, Config, $ planService.updateSubscription = function($scope, orgname, planId, success, failure) { if (!Features.BILLING) { return; } - + var subscriptionDetails = { plan: planId, }; - + planService.getPlan(planId, function(plan) { ApiService.updateSubscription(orgname, subscriptionDetails).then( function(resp) { @@ -214,7 +214,7 @@ function(KeyService, UserService, CookieService, ApiService, Features, Config, $ failure ); }); - }; + }; planService.getCardInfo = function(orgname, callback) { if (!Features.BILLING) { return; } @@ -297,7 +297,7 @@ function(KeyService, UserService, CookieService, ApiService, Features, Config, $ callbacks['success'](resp) document.location = resp.url; }, 250); - + }, function(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; }]); diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 1a995e6cd9..9407342683 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -37,9 +37,11 @@ OrganizationCard, OrganizationPlan, OrganizationRhSku, + OrganizationRhSkuBatchRemoval, OrganizationRhSkuSubscriptionField, UserCard, UserPlan, + UserSkuList, ) from endpoints.api.build import ( RepositoryBuildList, @@ -5076,7 +5078,7 @@ def test_bind_sku_to_org(self): 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 @@ def test_bind_sku_duplicate(self): 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 @@ def test_bind_sku_unauthorized(self): 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 @@ def test_remove_sku_from_org(self): 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 @@ def test_remove_sku_from_org(self): ) 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() diff --git a/workers/test/test_reconciliationworker.py b/workers/test/test_reconciliationworker.py index cd0dce469a..6605f11a53 100644 --- a/workers/test/test_reconciliationworker.py +++ b/workers/test/test_reconciliationworker.py @@ -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")
Plan