From 19a20a6c94efb9b659ffa11f17ce5ffcc380ba00 Mon Sep 17 00:00:00 2001
From: Joseph Schorr <jschorr@gmail.com>
Date: Sun, 6 Apr 2014 00:36:19 -0400
Subject: [PATCH] Turn off all references and API calls to billing if the
 feature is disabled

---
 endpoints/api/__init__.py             |  6 ++
 endpoints/api/billing.py              | 12 +++-
 endpoints/api/organization.py         |  5 +-
 endpoints/api/subscribe.py            |  4 ++
 endpoints/api/user.py                 | 11 ++--
 endpoints/web.py                      |  4 ++
 static/directives/header-bar.html     |  2 +-
 static/js/app.js                      | 26 ++++++--
 static/js/controllers.js              | 91 ++++++++++++++++-----------
 static/partials/new-organization.html |  7 ++-
 static/partials/org-admin.html        | 24 ++++---
 static/partials/user-admin.html       |  5 +-
 12 files changed, 135 insertions(+), 62 deletions(-)

diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py
index 61c1f3e6a..97f766da7 100644
--- a/endpoints/api/__init__.py
+++ b/endpoints/api/__init__.py
@@ -124,6 +124,9 @@ def format_date(date):
 
 def add_method_metadata(name, value):
   def modifier(func):
+    if func is None:
+      return None
+
     if '__api_metadata' not in dir(func):
       func.__api_metadata = {}
     func.__api_metadata[name] = value
@@ -132,6 +135,9 @@ def add_method_metadata(name, value):
 
 
 def method_metadata(func, name):
+  if func is None:
+    return None
+
   if '__api_metadata' in dir(func):
     return func.__api_metadata.get(name, None)
   return None
diff --git a/endpoints/api/billing.py b/endpoints/api/billing.py
index 1f31aa58b..89dda31f0 100644
--- a/endpoints/api/billing.py
+++ b/endpoints/api/billing.py
@@ -4,13 +4,14 @@ from flask import request
 
 from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
                            related_user_resource, internal_only, Unauthorized, NotFound,
-                           require_user_admin)
+                           require_user_admin, show_if, hide_if)
 from endpoints.api.subscribe import subscribe, subscription_view
 from auth.permissions import AdministerOrganizationPermission
 from auth.auth_context import get_authenticated_user
 from data import model
 from data.plans import PLANS
 
+import features
 
 def carderror_response(e):
   return {'carderror': e.message}, 402
@@ -79,6 +80,7 @@ def get_invoices(customer_id):
 
 
 @resource('/v1/plans/')
+@show_if(features.BILLING)
 class ListPlans(ApiResource):
   """ Resource for listing the available plans. """
   @nickname('listPlans')
@@ -91,6 +93,7 @@ class ListPlans(ApiResource):
 
 @resource('/v1/user/card')
 @internal_only
+@show_if(features.BILLING)
 class UserCard(ApiResource):
   """ Resource for managing a user's credit card. """
   schemas = {
@@ -132,6 +135,7 @@ class UserCard(ApiResource):
 @resource('/v1/organization/<orgname>/card')
 @internal_only
 @related_user_resource(UserCard)
+@show_if(features.BILLING)
 class OrganizationCard(ApiResource):
   """ Resource for managing an organization's credit card. """
   schemas = {
@@ -178,6 +182,7 @@ class OrganizationCard(ApiResource):
 
 @resource('/v1/user/plan')
 @internal_only
+@show_if(features.BILLING)
 class UserPlan(ApiResource):
   """ Resource for managing a user's subscription. """
   schemas = {
@@ -234,6 +239,7 @@ class UserPlan(ApiResource):
 @resource('/v1/organization/<orgname>/plan')
 @internal_only
 @related_user_resource(UserPlan)
+@show_if(features.BILLING)
 class OrganizationPlan(ApiResource):
   """ Resource for managing a org's subscription. """
   schemas = {
@@ -294,6 +300,7 @@ class OrganizationPlan(ApiResource):
 
 @resource('/v1/user/invoices')
 @internal_only
+@show_if(features.BILLING)
 class UserInvoiceList(ApiResource):
   """ Resource for listing a user's invoices. """
   @require_user_admin
@@ -310,6 +317,7 @@ class UserInvoiceList(ApiResource):
 @resource('/v1/organization/<orgname>/invoices')
 @internal_only
 @related_user_resource(UserInvoiceList)
+@show_if(features.BILLING)
 class OrgnaizationInvoiceList(ApiResource):
   """ Resource for listing an orgnaization's invoices. """
   @nickname('listOrgInvoices')
@@ -323,4 +331,4 @@ class OrgnaizationInvoiceList(ApiResource):
         
       return get_invoices(organization.stripe_id)
 
-    raise Unauthorized()
\ No newline at end of file
+    raise Unauthorized()
diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py
index 9cb6a267a..f89ddc5d5 100644
--- a/endpoints/api/organization.py
+++ b/endpoints/api/organization.py
@@ -5,7 +5,7 @@ from flask import request
 
 from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
                            related_user_resource, internal_only, Unauthorized, NotFound,
-                           require_user_admin, log_action)
+                           require_user_admin, log_action, show_if)
 from endpoints.api.team import team_view
 from endpoints.api.user import User, PrivateRepositories
 from auth.permissions import (AdministerOrganizationPermission,  OrganizationMemberPermission,
@@ -15,6 +15,8 @@ from data import model
 from data.plans import get_plan
 from util.gravatar import compute_hash
 
+import features
+
 
 logger = logging.getLogger(__name__)
 
@@ -163,6 +165,7 @@ class Organization(ApiResource):
 @resource('/v1/organization/<orgname>/private')
 @internal_only
 @related_user_resource(PrivateRepositories)
+@show_if(features.BILLING)
 class OrgPrivateRepositories(ApiResource):
   """ Custom verb to compute whether additional private repositories are available. """
   @nickname('getOrganizationPrivateAllowed')
diff --git a/endpoints/api/subscribe.py b/endpoints/api/subscribe.py
index f9f9d7f14..efc2dfea7 100644
--- a/endpoints/api/subscribe.py
+++ b/endpoints/api/subscribe.py
@@ -6,6 +6,7 @@ from endpoints.common import check_repository_usage
 from data import model
 from data.plans import PLANS
 
+import features
 
 logger = logging.getLogger(__name__)
 
@@ -24,6 +25,9 @@ def subscription_view(stripe_subscription, used_repos):
 
 
 def subscribe(user, plan, token, require_business_plan):
+  if not features.BILLING:
+    return
+
   plan_found = None
   for plan_obj in PLANS:
     if plan_obj['stripeId'] == plan:
diff --git a/endpoints/api/user.py b/endpoints/api/user.py
index f89d7ed62..40194a436 100644
--- a/endpoints/api/user.py
+++ b/endpoints/api/user.py
@@ -194,6 +194,7 @@ class User(ApiResource):
 
 @resource('/v1/user/private')
 @internal_only
+@show_if(features.BILLING)
 class PrivateRepositories(ApiResource):
   """ Operations dealing with the available count of private repositories. """
   @require_user_admin
@@ -249,8 +250,7 @@ class ConvertToOrganization(ApiResource):
       'description': 'Information required to convert a user to an organization.',
       'required': [
         'adminUser',
-        'adminPassword',
-        'plan',
+        'adminPassword'
       ],
       'properties': {
         'adminUser': {
@@ -263,7 +263,7 @@ class ConvertToOrganization(ApiResource):
         },
         'plan': {
           'type': 'string',
-          'description': 'The plan to which the organizatino should be subscribed',
+          'description': 'The plan to which the organization should be subscribed',
         },
       },
     },
@@ -290,8 +290,9 @@ class ConvertToOrganization(ApiResource):
                            message='The admin user credentials are not valid')
 
     # Subscribe the organization to the new plan.
-    plan = convert_data['plan']
-    subscribe(user, plan, None, True)  # Require business plans
+    if features.BILLING:
+      plan = convert_data.get('plan', 'free')
+      subscribe(user, plan, None, True)  # Require business plans
 
     # Convert the user to an organization.
     model.convert_user_to_organization(user, model.get_user(admin_username))
diff --git a/endpoints/web.py b/endpoints/web.py
index 1e78b12af..e14c70e79 100644
--- a/endpoints/web.py
+++ b/endpoints/web.py
@@ -20,6 +20,8 @@ from util.names import parse_repository_name
 from util.gravatar import compute_hash
 from auth import scopes
 
+import features
+
 logger = logging.getLogger(__name__)
 
 web = Blueprint('web', __name__)
@@ -54,6 +56,7 @@ def snapshot(path = ''):
 
 @web.route('/plans/')
 @no_cache
+@route_show_if(features.BILLING)
 def plans():
   return index('')
 
@@ -152,6 +155,7 @@ def privacy():
 
 
 @web.route('/receipt', methods=['GET'])
+@route_show_if(features.BILLING)
 def receipt():
   if not current_user.is_authenticated():
     abort(401)
diff --git a/static/directives/header-bar.html b/static/directives/header-bar.html
index 1a6ade0b7..05f7e24cf 100644
--- a/static/directives/header-bar.html
+++ b/static/directives/header-bar.html
@@ -14,7 +14,7 @@
     <li><a ng-href="/repository/" target="{{ appLinkTarget() }}">Repositories</a></li>
     <li><a href="http://docs.quay.io/" target="_blank">Docs</a></li>
     <li><a ng-href="/tutorial/" target="{{ appLinkTarget() }}">Tutorial</a></li>
-    <li><a ng-href="/plans/" target="{{ appLinkTarget() }}">Pricing</a></li>
+    <li><a ng-href="/plans/" target="{{ appLinkTarget() }}" quay-require="['BILLING']">Pricing</a></li>
     <li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
   </ul>
 
diff --git a/static/js/app.js b/static/js/app.js
index 532512bb0..4c60e1c4d 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -876,8 +876,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
       return keyService;
     }]);
 
-    $provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService',
-        function(KeyService, UserService, CookieService, ApiService) {
+    $provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features',
+        function(KeyService, UserService, CookieService, ApiService, Features) {
       var plans = null;
       var planDict = {};
       var planService = {};
@@ -903,7 +903,9 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
       };
 
       planService.notePlan = function(planId) {
-        CookieService.putSession('quay.notedplan', planId);
+        if (Features.BILLING) {
+          CookieService.putSession('quay.notedplan', planId);
+        }
       };
 
       planService.isOrgCompatible = function(plan) {
@@ -929,7 +931,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
 
       planService.handleNotedPlan = function() {
         var planId = planService.getAndResetNotedPlan();
-        if (!planId) { return false; }
+        if (!planId || !Features.BILLING) { return false; }
 
         UserService.load(function() {
           if (UserService.currentUser().anonymous) {
@@ -974,6 +976,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
       };
 
       planService.verifyLoaded = function(callback) {
+        if (!Features.BILLING) { return; }
+
         if (plans) {
           callback(plans);
           return;
@@ -1033,10 +1037,14 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
       };
 
       planService.getSubscription = function(orgname, success, failure) {
-         ApiService.getSubscription(orgname).then(success, failure);
+        if (!Features.BILLING) { return; }
+
+        ApiService.getSubscription(orgname).then(success, failure);
       };
 
       planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
+        if (!Features.BILLING) { return; }
+
         var subscriptionDetails = {
           plan: planId
         };
@@ -1056,6 +1064,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
       };
 
       planService.getCardInfo = function(orgname, callback) {
+        if (!Features.BILLING) { return; }
+
         ApiService.getCard(orgname).then(function(resp) {
           callback(resp.card);
         }, function() {
@@ -1064,6 +1074,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
       };
 
       planService.changePlan = function($scope, orgname, planId, callbacks) {
+        if (!Features.BILLING) { return; }
+
         if (callbacks['started']) {
           callbacks['started']();
         }
@@ -1089,6 +1101,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
       };
 
       planService.changeCreditCard = function($scope, orgname, callbacks) {
+        if (!Features.BILLING) { return; }
+
         if (callbacks['opening']) {
           callbacks['opening']();
         }
@@ -1145,6 +1159,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
       };
 
       planService.showSubscribeDialog = function($scope, orgname, planId, callbacks) {
+        if (!Features.BILLING) { return; }
+
         if (callbacks['opening']) {
           callbacks['opening']();
         }
diff --git a/static/js/controllers.js b/static/js/controllers.js
index 08bcca561..33b17772f 100644
--- a/static/js/controllers.js
+++ b/static/js/controllers.js
@@ -1607,7 +1607,9 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams
 }
 
 function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, UserService, CookieService, KeyService,
-    $routeParams, $http) {
+                       $routeParams, $http, Features) {
+  $scope.Features = Features;
+
   if ($routeParams['migrate']) {
     $('#migrateTab').tab('show')
   }
@@ -1690,13 +1692,15 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
   };
 
   $scope.showConvertForm = function() {
-    PlanService.getMatchingBusinessPlan(function(plan) {
-      $scope.org.plan = plan;
-    });
+    if (Features.BILLING) {
+      PlanService.getMatchingBusinessPlan(function(plan) {
+        $scope.org.plan = plan;
+      });
 
-    PlanService.getPlans(function(plans) {
-      $scope.orgPlans = plans;
-    });
+      PlanService.getPlans(function(plans) {
+        $scope.orgPlans = plans;
+      });
+    }
       
     $scope.convertStep = 1;
   };
@@ -1711,7 +1715,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
     var data = {
       'adminUser': $scope.org.adminUser,
       'adminPassword': $scope.org.adminPassword,
-      'plan': $scope.org.plan.stripeId
+      'plan': $scope.org.plan ? $scope.org.plan.stripeId : ''
     };
 
     ApiService.convertUserToOrganization(data).then(function(resp) {
@@ -1912,7 +1916,7 @@ function V1Ctrl($scope, $location, UserService) {
   UserService.updateUserIn($scope);
 }
 
-function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService) {
+function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService, Features) {
   UserService.updateUserIn($scope);
 
   $scope.githubRedirectUri = KeyService.githubRedirectUri;
@@ -2034,13 +2038,19 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
   var checkPrivateAllowed = function() {
     if (!$scope.repo || !$scope.repo.namespace) { return; }
 
+    if (!Features.BILLING) {
+      $scope.checkingPlan = false;
+      $scope.planRequired = null;
+      return;
+    }
+
     $scope.checkingPlan = true;
 
     var isUserNamespace = $scope.isUserNamespace;
     ApiService.getPrivateAllowed(isUserNamespace ? null : $scope.repo.namespace).then(function(resp) {
       $scope.checkingPlan = false;
  
-      if (resp['privateAllowed']) {        
+      if (resp['privateAllowed']) {
         $scope.planRequired = null;
         return;
       }
@@ -2160,18 +2170,20 @@ function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) {
   loadOrganization();
 }
 
-function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService) {
+function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService, Features) {
   var orgname = $routeParams.orgname;
 
   // Load the list of plans.
-  PlanService.getPlans(function(plans) {
-    $scope.plans = plans;
-    $scope.plan_map = {};
-
-    for (var i = 0; i < plans.length; ++i) {
-      $scope.plan_map[plans[i].stripeId] = plans[i];
-    }
-  });
+  if (Features.BILLING) {
+    PlanService.getPlans(function(plans) {
+      $scope.plans = plans;
+      $scope.plan_map = {};
+      
+      for (var i = 0; i < plans.length; ++i) {
+        $scope.plan_map[plans[i].stripeId] = plans[i];
+      }
+    });
+  }
 
   $scope.orgname = orgname;
   $scope.membersLoading = true;
@@ -2354,30 +2366,39 @@ function OrgsCtrl($scope, UserService) {
   browserchrome.update();
 }
 
-function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService) {
+function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService, Features) {
+  $scope.Features = Features;
+  $scope.holder = {};
+
   UserService.updateUserIn($scope);
 
   var requested = $routeParams['plan'];
 
-  // Load the list of plans.
-  PlanService.getPlans(function(plans) {
-    $scope.plans = plans;
-    $scope.currentPlan = null;
-    if (requested) {
-      PlanService.getPlan(requested, function(plan) {
-        $scope.currentPlan = plan;
-      });
-    }
-  });
+  if (Features.BILLING) {
+    // Load the list of plans.
+    PlanService.getPlans(function(plans) {
+      $scope.plans = plans;
+      $scope.currentPlan = null;
+      if (requested) {
+        PlanService.getPlan(requested, function(plan) {
+          $scope.currentPlan = plan;
+        });
+      }
+    });
+  }
 
   $scope.signedIn = function() {
-    PlanService.handleNotedPlan();
+    if (Features.BILLING) {
+      PlanService.handleNotedPlan();
+    }
   };
 
   $scope.signinStarted = function() {
-    PlanService.getMinimumPlan(1, true, function(plan) {
-      PlanService.notePlan(plan.stripeId);
-    });
+    if (Features.BILLING) {
+      PlanService.getMinimumPlan(1, true, function(plan) {
+        PlanService.notePlan(plan.stripeId);
+      });
+    }
   };
 
   $scope.setPlan = function(plan) {
@@ -2409,7 +2430,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
       };
 
       // If the selected plan is free, simply move to the org page.
-      if ($scope.currentPlan.price == 0) {
+      if (!Features.BILLING || $scope.currentPlan.price == 0) {
         showOrg();
         return;
       }
diff --git a/static/partials/new-organization.html b/static/partials/new-organization.html
index be99bae98..5f4756cfe 100644
--- a/static/partials/new-organization.html
+++ b/static/partials/new-organization.html
@@ -66,13 +66,14 @@
           </div>
 
           <!-- Plans Table -->
-          <div class="form-group nested plan-group">
+          <div class="form-group nested plan-group" quay-require="['BILLING']">
            <strong>Choose your organization's plan</strong>
-           <div class="plans-table" plans="plans" current-plan="currentPlan"></div>          
+           <div class="plans-table" plans="plans" current-plan="holder.currentPlan"></div>          
           </div>
            
           <div class="button-bar">
-            <button class="btn btn-large btn-success" type="submit" ng-disabled="newOrgForm.$invalid || !currentPlan"
+            <button class="btn btn-large btn-success" type="submit"
+                    ng-disabled="newOrgForm.$invalid || (Features.BILLING && !holder.currentPlan)"
                     analytics-on analytics-event="create_organization">
               Create Organization
             </button>
diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html
index 139579aa8..2b7b0be80 100644
--- a/static/partials/org-admin.html
+++ b/static/partials/org-admin.html
@@ -6,15 +6,23 @@
     <!-- Side tabs -->
     <div class="col-md-2">
       <ul class="nav nav-pills nav-stacked">
-        <li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
-        <li><a href="javascript:void(0)" data-toggle="tab" data-target="#settings">Organization Settings</a></li>
+        <li class="active" quay-require="['BILLING']">
+          <a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a>
+        </li>
+        <li quay-classes="{'!BILLING': 'active'}">
+          <a href="javascript:void(0)" data-toggle="tab" data-target="#settings">Organization Settings</a>
+        </li>
         <li><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
         <li><a href="javascript:void(0)" data-toggle="tab" data-target="#members" ng-click="loadMembers()">Members</a></li>
         <li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
         <li><a href="javascript:void(0)" data-toggle="tab" data-target="#prototypes">Default Permissions</a></li>
         <li><a href="javascript:void(0)" data-toggle="tab" data-target="#applications" ng-click="loadApplications()">Applications</a></li>
-        <li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing</a></li>
-        <li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li>
+        <li ng-show="hasPaidPlan" quay-require="['BILLING']">
+          <a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing</a>
+        </li>
+        <li ng-show="hasPaidPlan" quay-require="['BILLING']">
+          <a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a>
+        </li>
       </ul>
     </div>
 
@@ -22,12 +30,12 @@
     <div class="col-md-10">
       <div class="tab-content">       
         <!-- Plans tab -->
-        <div id="plan" class="tab-pane active">
+        <div id="plan" class="tab-pane active" quay-require="['BILLING']">
           <div class="plan-manager" organization="orgname" plan-changed="planChanged(plan)"></div>
         </div>
 
         <!-- Organization settings tab -->
-        <div id="settings" class="tab-pane">
+        <div id="settings" class="tab-pane" quay-classes="{'!BILLING': 'active'}">
           <div class="quay-spinner" ng-show="changingOrganization"></div>
 
           <div class="panel" ng-show="!changingOrganization">
@@ -67,12 +75,12 @@
         </div>
 
         <!-- Billing Options tab -->
-        <div id="billingoptions" class="tab-pane">
+        <div id="billingoptions" class="tab-pane" quay-require="['BILLING']">
           <div class="billing-options" organization="organization"></div>
         </div>
 
         <!-- Billing History tab -->
-        <div id="billing" class="tab-pane">
+        <div id="billing" class="tab-pane" quay-require="['BILLING']">
           <div class="billing-invoices" organization="organization" visible="invoicesShown"></div>
         </div>
 
diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html
index fcfb157c1..0a0bb1f82 100644
--- a/static/partials/user-admin.html
+++ b/static/partials/user-admin.html
@@ -243,13 +243,14 @@
               </div>
 
               <!-- Plans Table -->
-              <div class="form-group plan-group">
+              <div class="form-group plan-group" quay-require="['BILLING']">
                 <label>Organization Plan</label>
                 <div class="plans-table" plans="orgPlans" current-plan="org.plan"></div>          
               </div>
 
               <div class="button-bar">
-                <button class="btn btn-large btn-danger" type="submit" ng-disabled="convertForm.$invalid || !org.plan"
+                <button class="btn btn-large btn-danger" type="submit"
+                        ng-disabled="convertForm.$invalid || (Features.BILLING && !org.plan)"
                         analytics-on analytics-event="convert_to_organization">
                   Convert To Organization
                 </button>