Table Of Contents

Using Sales Channels

In the previous steps, you completed the integration of GitHub with the platform using a simplified sales scenario and sales model. In addition to direct sales, many companies effectively use their partnership with multiple sales channels to distribute their services as explained in the Customization for Sales Channels concepts. Based on those concepts, this document helps you enable your integration package to recognize resellers and effectively involve them in the wide distribution of the application services through their channels.

Prerequisites

Before continuing with the practical steps, ensure the following:

  • You have completed the development of the integration package with a simple sales model and your APS application successfully passed the tests as described in the previous steps of this demo project.

  • In addition to the cloud application account that your test provider used in the previous steps, you have one more account in GitHub that you will use as a test reseller.

Sales Model

Following this document, you will enable resellers to resell service plans delegated by their sales vendor (in our demo, it will be the provider) using their own resources in the cloud application. They can do this if you transform the application login and password parameters to “whitelabel” properties that resellers can change on their own. As a result, the reseller’s customers will consume the reseller’s resources in the cloud application opposed to the provider’s direct customers who consume the provider’s resources in the same cloud application.

Resource Model

The designed sales model maps to the following typical APS resource model derived from the previous Resource Model and extended with the reseller profile APS resource:

../../../_images/reseller-model.png

In the updated model, a reseller profile APS resource must bring the credentials of the respective reseller to the tenant provisioning logic.

Note

This resource must be configured by every sale vendor, including the provider, who wants to use their own credentials in the cloud application.

Development

The steps in this section walk you through the package updating process.

Metadata

At the bottom of the APP-META.xml file, declare a new service that will manage the reseller profile APS type:

<service id="resellers">
     <code engine="php" path="scripts/resellers.php"/>
     <presentation>
         <name>Reseller profile</name>
         <summary>Product customization for resellers</summary>
     </presentation>
 </service>

This declares a new provisioning logic that must be implemented by the scripts/resellers.php file through the service exposed on the APS bus as https://<endpoint_host>:<port>/<app_base_path>/resellers. For example, when you deploy the respective APS connector in a Docker container, the service endpoint will look similar to https://10.31.72.193:32770/endpoint/resellers.

The updated metadata declaration must look similar to the sample APP-META.xml(download) file:

<application xmlns="http://aps-standard.org/ns/2" version="2.0">
    <id>http://aps-standard.org/samples/github/</id>
    <name>GitHub Integration Demo</name>
    <version>1.0</version>
    <release>0</release>
    <vendor>
        <name>APS Team</name>
        <homepage>http://docs.cloudblue.com/oa/8.0/sdk/apps/github/</homepage>
    </vendor>
	<packager>
        <name>APS Team</name>
    	<homepage>http://docs.cloudblue.com/oa/8.0/sdk/apps/github/</homepage>
    </packager>
    <presentation>
        <summary>Integrates an external system based on REST API</summary>
        <description>This demo package is a part of the respective demo-project illustrating
            step-by-step design and development of an APS package for integrating
            a GitHub account with the CloudBlue platform. SKUs are simulated by GitHub
            access tokens with different scopes assigned to those tokens.
            Within the obtained scope, subscribers can manage GitHub repositories.
        </description>
        <categories>
           <category>Samples</category>
    	</categories>
        <navigation id="pcp" label="License Management">
            <var type-id="http://aps-standard.org/samples/github/app/1.0" name="app" />
            <plugs-to id="http://www.aps-standard.org/ui/application" />
            <item id="licenses" label="License Management">
                <view id="licenses" label="Licenses">
                    <view id="license-new" label="New License">
                        <controls>
                            <cancel />
                            <submit />
                        </controls>
                    </view>
                    <view id="license-edit" label="License {license.name}">
                        <controls>
                            <cancel />
                            <submit />
                        </controls>
                        <var type-id="http://aps-standard.org/samples/github/license/1.0"
                             name="license" />
                    </view>
                </view>
            </item>
        </navigation>
        <navigation id="ux1" label="Using application services">
            <view id="services" label="Application Services" src="ui/services.js"/>
            <var name="tenant" type-id="http://aps-standard.org/samples/github/tenant/1.0"
                 filter = "select(license)"/>
            <plugs-to id="http://www.parallels.com/ccp/2"/>
        </navigation>
    </presentation>
    <license-agreement must-accept="true">
		<free/>
		<text>
			<name>End-User License Agreement</name>
			<file>http://opensource.org/licenses/bsd-license</file>
		</text>
    </license-agreement>
    <upgrade match="version=ge=1.0, release=ge=0"/>
    <service id="app">
        <code engine="php" path="scripts/app.php"/>
        <presentation>
            <name>Application instance service</name>
            <summary>Connects to the application with the provider credentials</summary>
        </presentation>
    </service>
    <service id="licenses">
        <code engine="php" path="scripts/licenses.php"/>
        <presentation>
            <name>License</name>
            <summary>A service profile offered to customers</summary>
        </presentation>
    </service>
    <service id="tenants">
        <schema path="schemas/tenant.schema"/>
        <presentation>
            <name>Customer service - main tenant service</name>
            <summary>Corresponds to a repository in the cloud application</summary>
        </presentation>
    </service>
    <service id="resellers">
        <code engine="php" path="scripts/resellers.php"/>
        <presentation>
            <name>Reseller profile</name>
            <summary>Product customization for resellers</summary>
        </presentation>
    </service>
</application>

APS Application Instance

In the app APS type used to create APS application instances, add a relationship with the reseller profile APS type. For this purpose, in the scripts/app.php file add the following link collection:

/**
 * @link("http://aps-standard.org/samples/github/reseller/1.0[]")
 */
 public $resellers;

The above declaration refers to the APS type ID that will be declared in the next section.

Since the login and password properties are not needed at the upper level anymore, remove them from the app class definition.

The updated script must look similar to the sample app.php(download) file:

<?php

	define('APS_DEVELOPMENT_MODE', true);
	require "aps/2/runtime.php";	

	/**
	* @type("http://aps-standard.org/samples/github/app/1.0")
	* @implements("http://aps-standard.org/types/core/application/1.0","http://odin.com/init-wizard/config/1.0")
	*/

	class app extends \APS\ResourceBase
	{
        /**
         * @link("http://aps-standard.org/samples/github/tenant/1.0[]")
         */
        public $tenants;

        /**
         * @link("http://aps-standard.org/samples/github/license/1.0[]")
         */
        public $licenses;

        /**
         * @link("http://aps-standard.org/samples/github/reseller/1.0[]")
         */
        public $resellers;

        /**
         * @type(string)
         * @title("BaseURL")
         * @required
         * @description("Base endpoint in the external system. Note: The final slash is required.")
         */
        public $baseURL = "https://api.github.com/";

        /**
         * @verb(GET)
         * @path("/getInitWizardConfig")
         * @access(admin, true)
         * @access(owner, true)
         * @access(referrer, true)
         */
        public function getInitWizardConfig()
        {
            $myfile = fopen("./wizard_data.json", "r") or die("Unable to open file!");
            $data = fread($myfile,filesize("./wizard_data.json"));
            fclose($myfile);
            return json_decode($data);
        }

        /**
         * @verb(GET)
         * @path("/testConnection")
         * @param(object,body)
         * @access(admin, true)
         * @access(owner, true)
         * @access(referrer, true)
         */
        public function testConnection($body)
        {
            return "";
        }

    }
?>

Reseller Profile

Here, you will declare a new APS type with the default service implementation in a new resellers.php(download) file:

<?php

	define('APS_DEVELOPMENT_MODE', true);
	require "aps/2/runtime.php";

	/**
	* @type("http://aps-standard.org/samples/github/reseller/1.0")
	* @implements("http://aps-standard.org/types/core/profile/reseller/1.0")
	*/
	class reseller extends \APS\ResourceBase
	{
        ## Strong link to the application instance
        /**
         * @link("http://aps-standard.org/samples/github/app/1.0")
         * @required
         */
        public $app;

        ## Collection of links with tenants
        /**
         * @link("http://aps-standard.org/samples/github/tenant/1.0[]")
         */
        public $tenants;

        /**
         * @type(string)
         * @title("Login")
         * @required
         * @description("Login name of the reseller in the cloud application, e.g., resellerOne")
         * @access(referrer,false)
         */
        public $login;

        /**
         * @type(string)
         * @title("Password")
         * @required
         * @encrypted
         * @description("Password of the reseller in the cloud application")
         * @access(referrer,false)
         */
        public $passwd;

    }
?>

Key:

  • The above code declares an APS type implementing the abstract ResellerProfile APS type.

  • Reseller profile APS resources to be created from this APS type will have a strong link (app) with the APS resource representing the APS application instance. In addition, this APS type has a collection of links with tenants.

  • The new APS type contains two properties (login and passwd) to replace the corresponding properties removed from the app APS type. This pair will be used as the application “whitelabel” properties for resellers.

  • To protect login and password from being transferred through a delegated service template from the provider to the underneath resellers, the access attribute disallows access to those properties for referrers.

Tenant Schema

For the reasons explained in the Meta Declaration, the tenant APS type is declared directly in the schemas/tenant.schema file. In that file, declare the relationship with the new reseller profile APS type:

"reseller": {
    "type": "http://aps-standard.org/samples/github/reseller/1.0",
    "required": true,
    "collection": false
}

The above declaration refers to the APS type ID that you declared in the previous section.

The updated schema must look similar to the sample tenant.schema(download) file:

{
    "apsVersion": "2.0",
    "name": "tenant",
    "id": "http://aps-standard.org/samples/github/tenant/1.0",
    "implements": [
        "http://aps-standard.org/types/core/subscription/service/1.0"
    ],
    "properties": {
        "baseURL": {
            "type": "string",
            "required": true,
            "title": "BaseURL",
            "description": "Base URL of the cloud application endpoint"
        },
        "token": {
            "type": "string",
            "required": true,
            "title": "Token",
            "description": "User token for the tenant generated by the cloud application"
        },
        "tokenId": {
            "type": "integer",
            "required": true,
            "title": "TokenID",
            "description": "User token ID for the tenant generated by the cloud application"
        },
        "scopes": {
            "type": "string",
            "title": "SecurityScopes",
            "description": "Access to the application objects. Depends on the acquired license."
        },
        "repoNamePrefix": {
            "type": "string",
            "required": true,
            "title": "RepoNamePrefix",
            "description": "The prefix to be added to every repository created by the tenant"
        },
        "userAgent": {
            "type": "string",
            "required": true,
            "title": "UserAgent",
            "description": "Sent in the User-Agent HTTP header as required by some operations of the application API"
        },
        "licenseCounter": {
            "type": "Counter",
            "title": "License Limit and Usage",
            "description": "The total number of repositories allowed by the license"
        }
    },
    "operations": {
        "getRepos": {
            "verb": "GET",
            "path": "/repos",
            "response": {
                "contentType": null,
                "type": "object",
                "items": null
            }
        },
        "refreshData": {
            "verb": "GET",
            "path": "/refreshdata",
            "response": {
                "contentType": null,
                "type": "object",
                "items": null
            }
        },
        "createRepo": {
            "verb": "POST",
            "path": "/repos",
            "response": {
                "contentType": null,
                "type": "object",
                "items": null
            },
            "parameters": {
                "name": {
                    "type": "string",
                    "required": true,
                    "kind": "body"
                }
            }
        },
        "removeRepo": {
            "verb": "DELETE",
            "path": "/repos/{name}",
            "parameters": {
                "name": {
                    "type": "string",
                    "required": true,
                    "kind": "path"
                }
            }
        },
        "onLimitChange": {
            "verb": "POST",
            "path": "/onLimitChange",
            "eventSubscription": {
                "event" : "http://parallels.com/aps/events/pa/subscription/limits/changed",
                "source": {
                    "type" : "http://parallels.com/aps/types/pa/subscription/1.0"
                }
            },
            "parameters": {
                "notification": {
                    "type": "http://aps-standard.org/types/core/resource/1.0#Notification",
                    "required": true,
                    "kind": "body"
                }
            }
        }
    },
    "structures": {
        "Counter": {
            "type": "object",
            "properties": {
                "limit": {
                    "type": "integer",
                    "title": "License Limit",
                    "description": "The total number of repositories allowed by the license"
                },
                "usage": {
                    "type": "integer",
                    "title": "License Usage",
                    "description": "The total number of created repositories"
                }
            }
        }
    },
    "relations": {
        "app": {
            "type": "http://aps-standard.org/samples/github/app/1.0",
            "required": true,
            "collection": false
        },
        "license": {
            "type": "http://aps-standard.org/samples/github/license/1.0",
            "collection": false
        },
        "reseller": {
          "type": "http://aps-standard.org/samples/github/reseller/1.0",
          "required": true,
          "collection": false
        }
    }
}

Tenant Provisioning Logic

When working with the cloud application, the tenants service defined in the scripts/tenants.php file must use the reseller’s credentials, not the provider’s.

Note

The above statement is valid for the provider’s direct customers as well, since the provider will also own and configure the reseller profile.

Update the scripts/tenants.php file as follows:

  1. In the tenant class definition, add a strong link with the reseller profile:

    /**
     * @link("http://aps-standard.org/samples/github/reseller/1.0")
     * @required
     */
     public $reseller;
    
  2. Throughout the file, replace all occurrences of $this->app->login and $this->app->passwd with $this->reseller->login and $this->reseller->passwd respectively.

The last update switches customers from the provider’s account to the reseller’s account on GitHub.

The updated script must look similar to the sample tenants.php(download) file:

<?php

	define('APS_DEVELOPMENT_MODE', true);
	require "aps/2/runtime.php";
	require "rest-utils.php";

	class Counter {
		/**
		 * @type("integer")
		 * @title("License Limit")
		 * @description("The total number of repositories allowed by the license")
		 */
		public $limit;

		/**
		 * @type("integer")
		 * @title("License Usage")
		 * @description("The total number of created repositories")
		 */
		public $usage;
	}

	/**
	* @type("http://aps-standard.org/samples/github/tenant/1.0")
	* @implements("http://aps-standard.org/types/core/subscription/service/1.0")
	*/

	class tenant extends \APS\ResourceBase
	{
		## Strong link to the application instance
		/**
		 * @link("http://aps-standard.org/samples/github/app/1.0")
		 * @required
		 */
		public $app;

        ## Strong link to the reseller
        /**
         * @link("http://aps-standard.org/samples/github/reseller/1.0")
         * @required
         */
        public $reseller;

        ## Weak link to a license; the provisioning logic must set the proper link
		/**
		 * @link("http://aps-standard.org/samples/github/license/1.0")
		 */
		public $license;

        /**
         * @type(string)
         * @title("BaseURL")
         * @description("Base URL of the cloud application endpoint")
         */
        public $baseURL;

		/**
		 * @type(string)
		 * @title("Token")
		 * @description("User token for the tenant generated by the cloud application")
		 */
		public $token;

		/**
		 * @type(integer)
		 * @title("TokenID")
		 * @description("User token ID for the tenant generated by the cloud application")
		 */
		public $tokenId;

		/**
		 * @type(string)
		 * @title("SecurityScopes")
		 * @description("Access to the application objects. Depends on the acquired license.")
		 */
		public $scopes;

		/**
		 * @type(string)
		 * @title("RepoNamePrefix")
		 * @description("The prefix to be added to every repository created by the tenant")
		 */
		public $repoNamePrefix;

		/**
		 * @type(string)
		 * @title("UserAgent")
		 * @description("Sent in the User-Agent HTTP header as required by some operations of the application API")
		 */
		public $userAgent;

		## This is a custom counter created for demo purposes only - to show the number of created repos in UI
		/**
		 * @type(Counter)
		 * @title("License Limit and Usage")
		 * @description("The total number of repositories allowed by the license")
		 */
		public $licenseCounter;


		public function provision() {
            $logger = \APS\LoggerRegistry::get();
            $companyName = $this->account->companyName;
            ## Interact with the APS controller for getting additional data
            $apsc = \APS\Request::getController();

            ## Get data from the subscription:
            $subscriptionId = $this->subscription->aps->id;
            $resources = json_decode($apsc->getIo()->sendRequest(\APS\Proto::GET,
            "/aps/2/resources/".$subscriptionId."/resources"));

            foreach($resources as $resource) {
                if($resource->apsType == "http://aps-standard.org/samples/github/license/1.0" &&
                      $resource->limit > 0) {
                    # Link the tenant with the license:
                    # $apsc2 = $apsc->impersonate($this);
                    $logger->info("Request for link: ".$this."/license with ".$resource->apsId);
                    $apsc->linkResource($this, "license", $resource->apsId);

                    # Initialize the license counter:
                    $this->licenseCounter = (object)array("limit"=>$resource->limit, "usage"=>0);
                    break;
                }
            }

            $this->scopes = $this->license->scopes;

            ## Get the API URL from the App instance
            # Ensure the slash at the end of the URL
            $this->baseURL = rtrim($this->app->baseURL, '/') . '/';

            ## Get login name and password from the reseller
            $login = $this->reseller->login;
            $passwd = $this->reseller->passwd;

            ## Use the customer's admin email as the User Agent for the application API
            $this->userAgent = $this->account->adminContact->email;

            ## HTTP method and URL:
            $verb = 'POST';
            $url = $this->baseURL . 'authorizations';

            ## Payload for the request
            $payload = array(
               'scopes' => $this->scopes,
               'note' => $companyName
            );

            ## Send REST request to generate a user token for the tenant
            $response = rest_request_basic($url, $verb, $login, $passwd, $this->userAgent, $payload);
            if($response) {
               $this->token = $response->token;
               $this->tokenId = $response->id;
            }

            ## Repo names to be created later must begin with the following prefix
            $this->repoNamePrefix = makeRepoName($companyName);
		}

      ## Custom method to retrieve repositories from the App side
		/**
         * @verb(GET)
         * @path("/repos")
         * @return(object)
         */
         public function getRepos() {

            $verb = 'GET';
            $url = $this->baseURL.'user/repos';
            $token = $this->token;

            $response = rest_request($url, $verb, $token, $this->userAgent);

            $returnData = [];

            if($response) {
                foreach ($response as $repo) {
                    if($repo->name )
                        $name = $repo->name;
                    if(strpos($name, $this->repoNamePrefix) === 0) {
                        array_push($returnData,
                            array(
                                "id" => $repo->id,
                                "name" => $repo->name,
                                "url" => $repo->html_url
                            ));
                    }
                }
            }

            return $returnData;
         }

        ## Custom method to refresh the repo usage in the license counter
        /**
         * @verb(GET)
         * @path("/refreshdata")
         * @return(object)
         */
        public function refreshData() {
            $this->licenseCounter->usage = count($this->getRepos());
            $apsc = \APS\Request::getController();
            $apsc->updateResource($this);
        }

        ## Custom method to create a Repository on the App side
        /**
         * @verb(POST)
         * @path("/repos")
         * @param(string,body)
         * @return(object)
         */
        public function createRepo($name) {
            $this->licenseCounter->usage = count($this->getRepos());
            if($this->licenseCounter->usage >= $this->licenseCounter->limit) return;

            $repoName = $this->repoNamePrefix.'_'.makeRepoName($name->name_suffix);

            $verb = 'POST';
            $url = $this->baseURL.'user/repos';
            $token = $this->token;
            $payload = array('name' => $repoName);
            $response = rest_request($url, $verb, $token, $this->userAgent, $payload);

            $this->licenseCounter->usage = count($this->getRepos());
            $apsc = \APS\Request::getController();
            $apsc->updateResource($this);

            return $response;
        }

        ## Custom method to remove a Repository on the App side
        /**
         * @verb(DELETE)
         * @path("/repos/{name}")
         * @param(string,path)
         */
        public function removeRepo($name) {
            $logger = \APS\LoggerRegistry::get();

            $verb = 'DELETE';
            $url = $this->baseURL.'repos/'.$this->reseller->login.'/'.$name;
            $token = $this->token;
            $agent = $this->userAgent;

            $response = rest_request($url, $verb, $token, $agent);
            if($response) {
                $logger->info("\nRESPONSE received by the requester: ".$response."\n");
            }
            else $logger->info("No response - see details in /var/log/httpd/error.log \n");

            ## Update the counter in the APS DB
            $this->licenseCounter->usage = count($this->getRepos());
            $apsc = \APS\Request::getController();
            $apsc->updateResource($this);

        }

        # Event handler - used to update the license counter when the subscription is updated
        /**
         * @verb(POST)
         * @path("/onLimitChange")
         * @param("http://aps-standard.org/types/core/resource/1.0#Notification",body)
         */
        public function onLimitChange($notification) {
            $apsc = \APS\Request::getController();

            ## Get data from the subscription:
            $subscriptionId = $this->subscription->aps->id;
            $resources = json_decode($apsc->getIo()->sendRequest(\APS\Proto::GET,
                "/aps/2/resources/".$subscriptionId."/resources"));

            foreach($resources as $resource) {
                if($this->license->aps->type == $resource->apsType) {
                    $this->licenseCounter->limit = $resource->limit;
                    break;
                }
            }
            $apsc->updateResource($this);
        }

        public function unprovision() {
            ## Get login name and password from the reseller for DELETE repos
            $login = $this->reseller->login;
            $passwd = $this->reseller->passwd;
            ## HTTP method and URL:
            $verb = 'DELETE';

            ## Parameters for GET repos
            $url = $this->baseURL.'user/repos';
            $token = $this->token;

            $repos = rest_request($url, 'GET', $token, $this->userAgent);
            foreach ($repos as $repo) {
                $name = $repo->name;
                if(strpos($name, $this->repoNamePrefix) === 0) {
                    $url = $this->baseURL.'repos/'.$this->reseller->login.'/'.$name;
                    rest_request_basic($url, $verb, $login, $passwd);
                }
            }

            $url = $this->baseURL.'authorizations/'.$this->tokenId;
            rest_request_basic($url, 'DELETE', $login, $passwd);
        }
    }
?>

Deployment

This time, the deployment contains not only application and product deployment, but also the reseller environment configuration in the platform.

Default Product Configuration

The product configuration data provided by the APS application must contain the reseller profile configuration. Typically, you should go through the following configuration steps:

APS Resource -> Resource Type -> Service Template

With that in mind, update the scripts/wizard_data.json file as follows:

  1. Between the apsResources and resourceTypes sections, insert this resellerProfiles configuration section:

    "resellerProfiles": [{
       "id": "idcced499b78e4d8",
       "apsType": "http://aps-standard.org/samples/github/reseller/1.0",
       "type": "http://aps-standard.org/types/core/profile/reseller/1.0",
       "fields": {
          "login": "login",
          "passwd": "passwd"
       },
       "relations": {
          "app": "idglobals"
       }
    }]
    

    The above code creates an APS resource based on the reseller profile APS type declared in your package.

  2. In the resourceTypes section, add a resource type referring to the APS resource you created in the previous step:

    {
       "name": "GitHub Integration Demo - Value added reseller",
       "id": -500006,
       "resClass": "rc.saas.service.link",
       "required": false,
       "rtfor": "http://aps-standard.org/types/core/profile/reseller/1.0",
       "actParams": {
          "resource_uid": "idcced499b78e4d8"
       }
    }
    
  3. In the serviceTemplate section, add the resource type you created in the previous step:

    {
      "limit": 1,
      "unlimited": false,
      "rtID": -500006
    }
    

The updated JSON representation must look similar to the sample wizard_data.json(download) file:

{
"defaults": {
	"appId": "http://aps-standard.org/samples/github/app/1.0",
	"apsName": "GitHub Integration Demo",
	"apsVersion": "2.2",
	"apsResources": [
		{
			"apsType":"http://aps-standard.org/samples/github/license/1.0",
			"type": "http://aps-standard.org/types/core/profile/service/1.0",
			"id": "idc2922c137c7a58",
			"fields": {
				"profileName": "Basic access level",
				"name": "Basic",
        "scopes": [
          "repo"
        ]
			},
			"relations": {
				"app": "idglobals"
			}
		},
		{
      "apsType":"http://aps-standard.org/samples/github/license/1.0",
      "type": "http://aps-standard.org/types/core/profile/service/1.0",
      "id": "idc2922c137c7a60",
      "fields": {
        "profileName": "Premium access level",
        "name": "Premium",
        "scopes": [
          "repo",
          "delete_repo"
        ]
      },
      "relations": {
        "app": "idglobals"
      }
		}
  ],
  "resellerProfiles": [{
    "id": "idcced499b78e4d8",
    "apsType": "http://aps-standard.org/samples/github/reseller/1.0",
    "type": "http://aps-standard.org/types/core/profile/reseller/1.0",
    "fields": {
      "login": "login",
      "passwd": "passwd"
    },
    "relations": {
      "app": "idglobals"
    }
  }],
  "resourceTypes": [
  		{
  			"name": "GitHub Integration Demo - App REF",
  			"id": -500001,
  			"resClass": "rc.saas.service.link",
  			"required": true,
  			"actParams": {
	  			"app_id": "idOAID",
	  			"resource_uid": "idglobals"
  			}
  		},
  		{
  			"name": "GitHub Integration Demo - Basic access level",
  			"id": -500002,
  			"resClass": "rc.saas.service.link",
  			"required": false,
  			"rtfor": "http://aps-standard.org/types/core/profile/service/1.0",
  			"actParams": {
  				"resource_uid": "idc2922c137c7a58"
  			}
  		},
			{
				"name": "GitHub Integration Demo - Premium access level",
				"id": -500004,
				"resClass": "rc.saas.service.link",
				"required": false,
				"rtfor": "http://aps-standard.org/types/core/profile/service/1.0",
				"actParams": {
					"resource_uid": "idc2922c137c7a60"
				}
			},
	    {
        "name": "GitHub Integration Demo - Tenant Environment",
        "id": -500005,
        "resClass": "rc.saas.service",
        "required": true,
        "actParams": {
           "service_id": "tenants",
           "autoprovide_service": 1
        }
      },
      {
        "name": "GitHub Integration Demo - Value added reseller",
        "id": -500006,
        "resClass": "rc.saas.service.link",
        "required": false,
        "rtfor": "http://aps-standard.org/types/core/profile/reseller/1.0",
        "actParams": {
          "resource_uid": "idcced499b78e4d8"
        }
      }
  ],
	"serviceTemplate": {
		"id": -600001,
		"name": "GitHub Integration Demo",
		"resources": [
			{
				"limit": 1,
				"unlimited": false,
				"rtID": -500001
			},
			{
				"limit": 0,
				"unlimited": false,
				"rtID": -500002
			},
			{
				"limit": 0,
				"unlimited": false,
				"rtID": -500004
			},
			{
				"limit": 1,
				"unlimited": false,
				"rtID": -500005
			},
      {
        "limit": 1,
        "unlimited": false,
        "rtID": -500006
      }
    ]
	},
	"billing": {
		"planCategory": {
			"id":-21,
			"name": "GitHub Integration Demo",
			"description": "Testing the integration with the GitHub API"
		}, 
		"salesCategory": {
			"id":-22,
			"inCCP": true,
			"name": "GitHub Integration Demo",
			"expand": true, 
			"description": "Testing the integration with the GitHub API"
		}, 
		"resourceCategory": {
			"id":-23,
			"optional": false,
			"name": "GitHub Integration Demo",
			"description": "Testing the integration with the GitHub API",
			"displayType": "radio"
		}
	}, 
	"servicePlans": [
		{
			"name": "GitHub Integration Demo - Basic Access Level",
			"id": -20,
			"stId": -600001,
			"shortDescription": "GitHub Integration - Basic Access Level",
			"longDescription": "Testing the integration with the GitHub API - Basic Access Level",
			"planBillingPeriod": 1, 
			"renewOrderInterval": 0, 
			"renewPointDays": 0, 
			"subscrPeriodType": 2, 
			"subscrRefundType": 0, 
			"subscrPeriod": 1, 
			"subscrRenewalFee": 0, 
			"subscrRecurringFee": 4.25, 
			"subscrDepositFee": 0, 
			"subscrTrial": false, 
			"subscrSetupFee": 2.0,
			"subscrTransferFee": 0,
			"resources": [{
					"id": -700002,
					"rtID": -500002,
					"inCP": true,
					"instore": true,
					"incl": 3,
					"min": 3,
					"max": -1, 
					"sFeePerUnit": true, 
					"rFeePerUnit": true, 
					"setupFee": 0, 
					"recFee": 1.5,
					"overFee": 2,
					"measurable": false
			}]
		},
		{
			"name": "GitHub Integration Demo - Premium Access Level",
			"id": -21,
			"stId": -600001,
			"shortDescription": "GitHub Integration - Premium Access Level",
			"longDescription": "Testing the integration with the GitHub API - Premium Access Level",
			"planBillingPeriod": 1,
			"renewOrderInterval": 0,
			"renewPointDays": 0,
			"subscrPeriodType": 2,
			"subscrRefundType": 0,
			"subscrPeriod": 1,
			"subscrRenewalFee": 0,
			"subscrRecurringFee": 4.25,
			"subscrDepositFee": 0,
			"subscrTrial": false,
			"subscrSetupFee": 2.0,
			"subscrTransferFee": 0,
			"resources": [{
					"id": -700004,
					"rtID": -500004,
					"inCP": true,
					"instore": true,
					"incl": 3,
					"min": 3,
					"max": -1,
					"sFeePerUnit": true,
					"rFeePerUnit": true,
					"setupFee": 0,
					"recFee": 2.5,
					"overFee": 3.0,
					"measurable": false
			}]
		}
	]
}
}

Packaging

The updated project is ready to be packaged. However, if you want to deploy the updated package on the same platform where the initial package is already deployed, you must change the IDs of the APS application and all its APS types. In other words, the updated APS application must look like a new APS application, different from the initial one. Another way is to go through the major upgrade of the initial APS application, but this is a more complicated process.

For example, in all those IDs, replace the samples/github string with the samples/github-reseller string. In addition, if you want to keep Docker images of both packages in the same Docker registry, change the imagename property in the deployment.xml file.

The packaging procedure consists of the following typical steps:

  1. Build the new Docker image and push it to the Docker registry as explained in the Create Image section.

  2. Build the APS package as described in the Build APS Package section.

Application Deployment

Deploy your new application on the platform using the following typical steps:

  1. Import the APS package as described in the Import APS Package section.

  2. Install an APS application instance as described in the Deploy APS Application Instance section. The difference in the new model is that this time, you must enter only BaseURL as an instance property.

Product Deployment

Configure the service plans and related components as described in the Create Products for Sale section. This time, the product configuration wizard will present one more step to configure the reseller profile. At this step, do not change the default reseller profile configuration and click Next”.

After completing the product deployment, you will have the following product components for you to use in the further deployment and provisioning steps:

  • A service template called “GitHub Integration Demo”

  • A service plan called “GitHub Integration Demo - Premium Access Level”

  • A service plan called “GitHub Integration Demo - Basic Access Level”

Ensure that their configuration is correct:

  1. In the OSS PCP, navigate to Products > Service Templates, open the new service template and verify that all necessary resources are on the Resources tab:

    ../../../_images/reseller-st-resources.png
  2. In the BSS PCP, navigate to Products > Service Plans, open one by one the new service plans and ensure each of them has only one resource rate:

    • The GitHub Integration Demo - Basic Access level service plan must have the GitHub Integration Demo - Basic access level resource rate.

      ../../../_images/reseller-sp-basic.png
    • The GitHub Integration Demo - Premium Access level service plan must have the GitHub Integration Demo - Premium access level resource rate.

      ../../../_images/reseller-sp-premium.png

    No more resource rates are needed here.

  3. Make both service plans ready for sale. For this, open each plan one by one and on the Publication Settings tab, ensure the plan is included into a sales category and the Published property is on.

Sales Channel

The reseller concepts are described in the Customization for Sales Channels document set. Follow these steps to create an L1 reseller to test your application (for details and panel screenshots, refer to the Deployment and Provisioning document):

  1. In the OSS Provider Control Panel (PCP), create a service template called, for example, “L1 Reseller resources” with the following resources to enable resellers to have their own customers and sub-resellers:

    • Client accounts

    • Reseller accounts

  2. In the BSS PCP, create one more service template based on the “Resellers Management” gateway and name it “L1 Reseller”. In the Branding Template property, specify the “L1 Reseller resources” service template that you created in the previous step.

  3. In the “L1 Reseller” service template, specify the service template and the service plans to delegate to resellers:

    • On the Delegated ST tab, add the “GitHub Integration Demo” service template.

      ../../../_images/reseller-delegated-st.png
    • On the Delegated Plans tab, add both application service plans you created previously:

      • “GitHub Integration Demo - Premium Access Level”

      • “GitHub Integration Demo - Basic Access Level”

      ../../../_images/reseller-delegated-sp.png
  4. Using the “L1 Reseller” service template, create a service plan to sell reseller status. For this purpose, on the Plans tab, click Add New Service Plan and go through the wizard steps to create a service plan named “L1 Reseller” with zero prices for a 1 year subscription period. Ensure the new plan is available for sale by including the service plan in a sales category. The best practice is to create a separate sales category for reseller plans.

  5. Create an L1 reseller by ordering the “L1 Reseller” service plan.

  6. Verify that a new reseller is created. For this, enter the OSS Reseller Control Panel (RCP) on behalf of the new reseller and then switch to the BSS RCP to initialize the reseller. Navigate to System > Settings and open the Payment Processing configuration to ensure that at least the Check/Cash payment system is enabled there.

  7. In the OSS RCP, navigate to Products > Service Templates, find the delegated service template called “GitHub Integration Demo” and open it. On the GitHub Integration Demo Configuration tab, enter the GitHub account login and password belonging to the reseller:

    ../../../_images/reseller-creds.png
  8. In the BSS RCP, navigate to Products > Service Plans to ensure there are two delegated service plans.

  9. As the provider, ensure the reseller has enough credit to resell the delegated service plans. For this purpose, in the BSS PCP, navigate to Operations > Resellers, open the reseller configuration screen and on the General tab, set a customized credit limit (for example, $1000) for the reseller.

Now your reseller is ready for testing the resale and provisioning process.

Resale and Provisioning

This final phase is similar to the one described in the Provisioning document. The only difference is that you start it in the reseller control panel instead of the provider panel. When following that process, your reseller will get a customer subscribed to the application services.

In the customer UX1 panel, you can operate the GitHub repositories as described in the Provisioning document.

../../../_images/reseller-repos.png

Note

The main difference in the new model is that the repositories for the reseller’s customer are created using the reseller account (apsdemoreseller) instead of the provider account (apsdemo) on GitHub.

Conclusion

This document describes the implementation of the typical reseller model in an APS application. The most important thing is to understand how the reseller profile APS type helps you create “whitelabel” properties that resellers can use to modify the application services for their customers. Your final integration package must look similar to the GitHub_Integration_Demo package.