Product Line Expansion

Software vendors (ISV) periodically add more features to their cloud applications and expose new services, and therefore want to expand their list of products for sale. By following this document, you will continue the development process by adding another function to the APS application to assist the product configuration manager in adding more products for sale.

Prerequisites

Before continuing with the practical steps, ensure you have completed the development of the integration package using a simple sales model or a model with resellers and your APS application successfully passed the tests as described in the previous steps of this demo project.

MPN

The platform is able to identify cloud services by MPN (Manufacturer Product Number ) assigned by the software vendors. After an ISV exposes new services or service combinations with unique MPNs and notifies the service provider about it, the latter initiates a process of expanding the product line to provide the new ISV services. During this process, the platform requests the respective APS connector for certain data used to expand the product line.

ISVs use their own MPN formats. In this project, an MPN must have three fields separated by “/” (slash):

  • GitHub - the prefix that identifies the ISV.

  • A list of GitHub scopes separated by “-” (dash). This field is the main identifier that defines the services available on GitHub.

  • A number - the suffix that allows the ISV to differentiate products which have the same list of scopes.

For the services considered in the previous steps, we can assign the following MPNs:

  • GitHub/repo-delete_repo/1001 - a license with the repo and delete_repo scopes.

  • GitHub/repo/1001 - a license with the repo scope.

To expand the product line, you will use one more scope called gist in combination with the previously used scopes. You will use it to add the following MPNs mapped to licenses:

  • GitHub/gist/1001 - a license for using the gist scope.

  • GitHub/repo-gist/1001 - a license for using the repo and gist scopes.

  • GitHub/repo-delete_repo-gist/1001 - a license for using the repo, delete_repo, and gist scopes.

Product Configuration File

To initiate the product line expansion process, the provider must have a configuration file in the CSV format that contains the initial data about the new services. There are certain requirements for file format and content. For this project, use the sample file (download) with the following content:

A/C/D/U,APS Type,Service Template,MPN,MSRP Recurring Fee,VAR Recurring Fee
ADD,http://aps-standard.org/samples/github/license/2.0,GitHub Integration Demo,GitHub/gist/2001,2.5,2.0
ADD,http://aps-standard.org/samples/github/license/2.0,GitHub Integration Demo,GitHub/repo-gist/2001,4.5,3.5
ADD,http://aps-standard.org/samples/github/license/2.0,GitHub Integration Demo,GitHub/repo-delete_repo-gist/2001,5.5,4.5

The above file is used for two actions in the platform:

  1. To request the APS application for the new product configuration.

  2. To assign prices to the newly created products.

Resource Model

The internal relationship in the APS resource model remains the same as used in the previous basic model or model with resellers. To support the required function, the following items must be changed in APS types:

  • The license APS type must declare one more property called mpn to store an MPN corresponding to the MPN on the ISV side. The other way is to implement the abstract APS type ItemProfile that contains the mpn property.

  • The application root APS type app must implement the InitWizardVendorCatalogConfig APS type and, particularly, the fetchCatalog operation. The platform will call this operation for the configuration of the new products to sell new licenses.

Development

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

Note

This will be a major update of the APS application. That is why you need to change the version of the APS application in the APP-Meta.xml file and the versions of all APS types in the respective *.php and *.schema files from 1.0 to 2.0. Along with that, update the APS links (provisioning logic) and the requests in JavaScript (presentation logic) to match the new versions.

MPN Mapping

The APS package must contain the mpn-settings.json file in its root folder. This file declares an APS property that the platform must use as the MPN for the products sold through the respective APS application. In this project, the license APS type will have the mpn property to be used as the application MPN. For this purpose, create the following content in the file:

{
  "http://aps-standard.org/samples/github/license/2.0": {
    "mpn": "MPN"
  }
}

Note

The “MPN” string must correspond to the “MPN” column header in the Product Configuration File.

Application License

As specified in Resource Model and License, the main property of a license is a list of scopes. Each combination of scopes must correspond to a certain MPN as stated in the MPN section. In accordance with MPN Mapping, add the following property definition in the scripts/licenses.php file:

## The property mapped to the MPN (Manufacturer Product Number)
## For each combination of scopes, there must be a unique MPN
/**
* @type(string)
* @title("ManufacturerProductNumber")
* @required
* @description("Manufacturer Product Number unique for this type of service")
*/
public $mpn;

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

<?php

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

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

	class license extends \APS\ResourceBase
	{
		/**
         * @link("http://aps-standard.org/samples/github/app/2.0")
         * @required
         */
        public $app;

        /**
         * @link("http://aps-standard.org/samples/github/tenant/2.0[]")
         */
        public $tenants;

        ## The license name to represent a product SKU
        /**
         * @type(string)
         * @title("LicenseName")
         * @required
         * @description("Human readable license name")
         */
        public $name;

        ## Just a human readable description.
        /**
         * @type(string)
         * @title("LicenseDescription")
         * @description("Human readable license description")
         */
        public $description;

        ## The only property that allows creating various App reference resources - App licenses.
        /**
         * @type(string[])
         * @title("Scopes")
         * @required
         * @description("Authorized scopes of resources on the App side for a token generated for a customer")
         */
        public $scopes = array("repo");

        ## The property mapped to the MPN (Manufacturer Product Number)
        ## For each combination of scopes, there must be a unique MPN
        /**
         * @type(string)
         * @title("ManufacturerProductNumber")
         * @required
         * @description("Manufacturer Product Number unique for this type of service")
         */
        public $mpn;

    }
?>

Response Template

The platform will request the APS application to return product configuration containing arrays with the following elements that the product configuration manager will use to create new products:

  • apsResources - a list of service profiles implemented as reference APS resources

  • resourceTypes - a list of resource types to be added to a service template

  • servicePlans - a list of new service plans

To avoid having the structures of the above elements in the program code, add those structures to an auxiliary file - scripts/fetch_catalog_template.json (download):

{
	"apsResource": {
		"apsType":"http://aps-standard.org/samples/github/license/2.0",
		"type": "http://aps-standard.org/types/core/profile/service/1.0",
		"id": "idc2922c137c7a80",
		"relations": {
			"app": "idglobals"
		}
	},
	"resourceType": {
		"name": "GitHub Integration Demo, scopes: %s",
		"id": -510002,
		"resClass": "rc.saas.service.link",
		"required": false
	},
	"servicePlan": {
		"name": "GitHub Integration Demo with scopes: %s",
		"id": -20,
		"shortDescription": "GitHub Integration, scopes: %s",
		"longDescription": "Testing the integration with the GitHub API, scopes: %s",
		"planBillingPeriod": 1,
		"renewOrderInterval": 0,
		"subscrPeriodType": 2,
		"subscrPeriod": 1
	}
}

The structures in the above template correspond to the structures declared in the InitWizardVendorCatalogConfig APS type. The absent properties will be added by the program code.

APS Application Instance

In the app APS type used to create APS application instances, add the required fetchCatalog operation by following this process:

  1. In the scripts/app.php file, declare the implementation of the InitWizardVendorCatalogConfig APS type:

    /**
     * @type("http://aps-standard.org/samples/github/app/2.0")
     * @implements("http://aps-standard.org/types/core/application/1.0","http://odin.com/init-wizard/config/1.0","http://odin.com/init-wizard-vendor-catalog/config/1.0")
     */
    class app extends \APS\ResourceBase
    {
       # ... Class definition
    }
    
  2. Inside the app class definition, declare the fetchCatalog operation:

    /**
     * @verb(POST)
     * @path("/fetchCatalog")
     * @param("http://odin.com/init-wizard-vendor-catalog/config/1.0#FetchCatalogRequest",body)
     * @access(admin, true)
     * @access(owner, true)
     * @access(referrer, true)
     */
    public function fetchCatalog($request)
    {
       # The function definition  will be added here later.
    }
    

    As specified in the InitWizardVendorCatalogConfig APS type and declared above, the input parameter in the fetchCatalog operation must be the FetchCatalogRequest structure.

    The following steps will define this function.

  3. In the function definition field, add the following preparation steps:

    • Validate the request. It must refer to the license APS type where the mpn property is declared.

    • Read the response template from the auxiliary fetch_catalog_template.json file.

    • Initialize three arrays to return: apsResources, resourceTypes, and servicePlans.

    if($request->apsType != "http://aps-standard.org/samples/github/license/2.0" ||
            count($request->offerMpns) == 0)
    
        throw new \Exception("Incorrect APS type, the 'http://aps-standard.org/samples/github/license/2.0' is expected");
    
    $jsondata = file_get_contents("./fetch_catalog_template.json");
    
    $tmpl = json_decode($jsondata);
    
    $apsResources = [];
    $resourceTypes = [];
    $servicePlans = [];
    
  4. Recursively process the MPNs in the input parameter to generate an apsResource, resourceType, and servicePlan for each MPN:

    foreach ($request->offerMpns as $mpn) {
        $reposArr = explode('-', explode('/', $mpn->mpn)[1]);
        $reposStr = implode(', ', $reposArr);
    
        $apsResource = clone $tmpl->apsResource;
        $resourceType = clone $tmpl->resourceType;
        $servicePlan = clone $tmpl->servicePlan;
    
        # Set an apsResource:
        $apsResource->id = 'idc'.(string)rand(1000000000000, 9999999999999);
        $apsResource->fields = new stdClass();
        $apsResource->fields->profileName = sprintf("Access with scopes: %s", $reposStr);
        $apsResource->fields->name = sprintf("Scopes: %s", $reposStr);
        $apsResource->fields->scopes = [];
        foreach ($reposArr as $repo) {
            array_push($apsResource->fields->scopes, $repo);
        }
        $apsResource->fields->mpn = $mpn->mpn;
    
        # Set a resourceType:
        $resourceType->name = sprintf($resourceType->name, $reposStr);
        $resourceType->id = rand(-599999, -500000);
        $resourceType->actParams = new stdClass();
        $resourceType->actParams->resource_uid = $apsResource->id;
    
        # Set a servicePlan:
        $servicePlan->name = sprintf($servicePlan->name, $reposStr);
        $servicePlan->id = rand(-999, -100);
        $servicePlan->shortDescription = sprintf($servicePlan->shortDescription, $reposStr);
        $servicePlan->longDescription = sprintf($servicePlan->longDescription, $reposStr);
        $servicePlan->resources = [(object)[
          'id' => rand(-799999, -700000),
          'rtID' => $resourceType->id,
          'incl' => 0,
          'min' => 1,
          'max' => -1
        ]];
    
        # Push the new objects to the response arrays:
        array_push($apsResources, clone $apsResource);
        array_push($resourceTypes, $resourceType);
        array_push($servicePlans, $servicePlan);
    }
    
  5. Group the required arrays in a JSON object and return the latter:

    $response = new stdClass();
    $response->apsResources = $apsResources;
    $response->resourceTypes = $resourceTypes;
    $response->servicePlans = $servicePlans;
    
    return $response;
    

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/2.0")
	* @implements("http://aps-standard.org/types/core/application/1.0","http://odin.com/init-wizard/config/1.0","http://odin.com/init-wizard-vendor-catalog/config/1.0")
	*/
	class app extends \APS\ResourceBase
	{
        /**
         * @link("http://aps-standard.org/samples/github/tenant/2.0[]")
         */
        public $tenants;

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

        /**
         * @link("http://aps-standard.org/samples/github/reseller/2.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(POST)
         * @path("/fetchCatalog")
         * @param("http://odin.com/init-wizard-vendor-catalog/config/1.0#FetchCatalogRequest",body)
         * @access(admin, true)
         * @access(owner, true)
         * @access(referrer, true)
         */
        public function fetchCatalog($request)
        {
            $logger = \APS\LoggerRegistry::get(); // Used for troubleshooting
            $logger->info("\nFETCH-CATALOG: Entered the function with request: ");
            if($request->apsType != "http://aps-standard.org/samples/github/license/2.0" ||
                    count($request->offerMpns) == 0)
                throw new \Exception("Incorrect APS type, the 'http://aps-standard.org/samples/github/license/2.0' is expected");

            $jsondata = file_get_contents("./fetch_catalog_template.json");
            $tmpl = json_decode($jsondata);

            $apsResources = [];
            $resourceTypes = [];
            $servicePlans = [];

            foreach ($request->offerMpns as $mpn) {
                $reposArr = explode('-', explode('/', $mpn->mpn)[1]);
                $reposStr = implode(', ', $reposArr);

                $apsResource = clone $tmpl->apsResource;
                $resourceType = clone $tmpl->resourceType;
                $servicePlan = clone $tmpl->servicePlan;

                # Setting a apsResource:
                $apsResource->id = 'idc'.(string)rand(1000000000000, 9999999999999);
                $apsResource->fields = new stdClass();
                $apsResource->fields->profileName = sprintf("Access with scopes: %s", $reposStr);
                $apsResource->fields->name = sprintf("Scopes: %s", $reposStr);
                $apsResource->fields->scopes = [];
                foreach ($reposArr as $repo) {
                    array_push($apsResource->fields->scopes, $repo);
                }
                $apsResource->fields->mpn = $mpn->mpn;

                # Setting a resourceType:
                $resourceType->name = sprintf($resourceType->name, $reposStr);
                $resourceType->id = rand(-599999, -500000);
                $resourceType->actParams = new stdClass();
                $resourceType->actParams->resource_uid = $apsResource->id;

                # Setting a servicePlan:
                $servicePlan->name = sprintf($servicePlan->name, $reposStr);
                $servicePlan->id = rand(-999, -100);
                $servicePlan->shortDescription = sprintf($servicePlan->shortDescription, $reposStr);
                $servicePlan->longDescription = sprintf($servicePlan->longDescription, $reposStr);
                $servicePlan->resources = [(object)[
                    'id' => rand(-799999, -700000),
                    'rtID' => $resourceType->id,
                    'incl' => 0,
                    'min' => 1,
                    'max' => -1
                ]];

                # Push the generated objects to the response arrays:
                array_push($apsResources, clone $apsResource);
                array_push($resourceTypes, $resourceType);
                array_push($servicePlans, $servicePlan);
            }

            # Response:
            $response = new stdClass();
            $response->apsResources = $apsResources;
            $response->resourceTypes = $resourceTypes;
            $response->servicePlans = $servicePlans;

            return $response;
        }

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

    }
?>

Deployment and Provisioning

The initial product deployment and provisioning are described in the Deployment and Provisioning documents of this project.

Upgrading the Product Line

Perform the following operations in the provider control panel (PCP) to test your updated APS application.

Verify MPN Mapping

In the updated APS application, the property mpn of the license APS type must map to the MPN field of the product configuration file. To verify this mapping, perform the following steps:

  1. In the OSS PCP, navigate to Services > Applications and open the updated APS application.

  2. On the Service Profiles tab, click MPN Settings.

  3. Ensure that the service profile property mpn maps to the CSV column name MPN:

../../../_images/mpn-mapping.png

The above configuration reflects the content of the ./mpn-settings.json file.

Get a New Product Configuration

Use the prepared product configuration file to request the APS application for the product configuration that enables the provider to sell the services whose MPNs are in that file. The process looks as follows:

  1. In the OSS PCP, navigate to Services > Applications and open the updated APS application.

  2. Click Configure Product, select the Extract vendor’s configuration option, and click Choose File to choose the product configuration file:

    ../../../_images/conf-file.png
  3. Click Next. On the next step, you will find a list of service profiles that represent all licenses to sell (those that exist in the platform and the new ones):

    ../../../_images/conf-step2.png
  4. Click Next until you reach the Service Plans step. For each new service plan proposed by the application, assign a proper category. For this purpose, click on the service plan name, select a plan category for it, and click Add:

    ../../../_images/conf-step5.png
  5. Click Next to accept the service plan configuration and then, on the final step, click Finish.

After this step, new service plans for selling new licenses provided by the integrated cloud application are added to the platform.

Import the Price

If you want to assign prices for the new products, the recommended way is to import a price configuration file as described in Updating Prices.

For testing purposes, use the same file that you used during the product configuration in the previous section and follow these steps:

  1. Ensure there is a custom attribute to set a country code to be used by the product configurator (InitWizard) when setting prices for sales vendors. For this purpose, in the BSS PCP, navigate to System > Settings and create an attribute whose ID and name are InitWizardCountryCode:

    ../../../_images/Initwizard-countrycode.png
  2. Every sales vendor must have this attribute configured. For this purpose, open the account settings and set the proper value for this custom attribute:

    ../../../_images/account-countrycode.png
  3. Ensure the name suffix of the CSV file to be used for importing prices is the same as the attribute you installed in the previous step, for example:

    $ cp  github_config_mpn.csv  github_config_mpn_us.csv
    
  4. Use that CSV file to import prices:

    • In the OSS PCP, open your APS application on the Service Profiles tab and click Import Prices.

    • Choose your CSV file, select the billing period as configured in your service plans, and click Next:

      ../../../_images/import-prices.png
    • Select the prices to import and click Submit:

      ../../../_images/selected-prices.png

Use the updated product configuration to subscribe customers to the application services.

Conclusion

In this project stage, you integrated your APS application closer with the platform product configuration manager. Now it can assist the provider in expanding the product line when the original cloud application exposes new services.

Your final integration package must look similar to the GitHub_Integration_Demo package.