Oracle Sales Cloud REST APIs – Handling Child Objects

Introduction

Oracle Sales Cloud provides a comprehensive set of customization tools and configuration options to implement customer-specific business cases. In this article I would like to put a spot light on an imaginary situation

  • Customer is active in an industry like Hightech, Mechanical Engineering or Tooling and uses Oracle Sales Cloud for their sales processes
  • As a new feature they would like to capture competitive intelligence for every existing account in Sales Cloud
  • The idea is to allow service technicians to collect information about installed base of competitor products at their customers site
  • This information can be gathered by service technicians via a mobile device (tablet, smartphone etc) using a custom application that is connected to Sales Cloud
  • Enrichment of information can happen later by sales employees via Oracle Sales Cloud UI – first step is creation of a catalogue by post-sales team for the creation of a base of competitive intelligence
  • We can imagine the benefits of such an information in a further sales cycle or for upsell activities

Such an extension can be easily implemented via Custom UI, Sales Cloud Application Composer and Sales Cloud REST interfaces. For custom UI multiple options exist such as using Oracle JET, or Ionic/HTML5 with AngularJS. My teammate Angelo Santagata has published recently a great article describing the interface between Oracle Sales Cloud and Oracle JET. Please refer to the article when you want learn more about Mobile UI creation via Oracle JET. In this article I’ll show a generic approach how a custom user interface built in HTML and Javascript could look like. The focus for this article will also be on Sales Cloud Child Objects and addressing them via REST interfaces.

High Level Steps

Before we start implementing such a solution our customer must be clear about the functional requirements:

  • which are the competitive data they want to collect and how will they be used later?
  • which data are mandatory in the moment new records are being created by service engineers and which data can be enriched later offline?
  • will the competitive information exist in a certain context of an account or rather represent a standalone data entity?

In a next step the new extension objects must be setup in AppComposer in Sales Cloud. Eventually a new page will be created that allows the creation and maintenance of the admired competitive information. So far no coding had to be done as all of these steps work on a declarative base.

These new objects will be addressable by REST API’s and the creation of a custom UI follows as a technical implementation step. A base decision has to be made about the technical implementation:

  • is there already a PaaS solution (MCS, JCS etc) in use and should a custom UI run on any of those platforms?
  • what are the preferred devices the service engineers use on their customers site?
  • are there any restrictions in terms of roles and policies setup for Account and their competitive data (who sees what)?
  • does our customer have the right skills in development team to create a custom solution?
  • do we have all the REST API’s we need?

Setting up Account related Child Objects in AppComposer

The best way to add Account related custom information can be done by creation of a Child Object in App Composer. In the beginning we must make a decision to create the data structures for this extension as Child Object or a Standalone Object. Standalone Objects are new objects that exist autonomously as new data entities in OSC, while Child Objects have a fixed relation to an existing Standard Object like Accounts, Addresses, Contacts, Sales Orders or others. In our specific use-case the definition of a Child Object under Accounts makes most sense as the additional information is tied to specific accounts and exist in that specific context. Standalone Objects might cause more overhead for creation of a relationship to existing Accounts. So there wouldn’t be a benefit to use them, while Child Objects fulfill exactly the requirements as needed in this business case.

Picture below shows the solution once a Child Object extension for storing competitive intelligence information has been set up. An additional icon represents a custom page to enter and maintain the competitive information for a specific Account. No coding is required as being explained further down in this article. Creation of such an extension is a pure declarative activity and data structures will resist lifecycle maintenance operations.

01_CustomerPage

Once the Child Objects have been created and user clicked on custom icon above, a new page will be opened to enter all known information about competitor products installed at this Account. Least number of information is mandatory like Competitor Name and a Remark with some qualifying information. A Service Engineer onsite might be under time pressure or have no access to other sources of competitive intelligence like pricing. We want him to enter the information he is capable to gather – sales employees can enrich the information at a later stage. Once a field in Child Object structure is registered as mandatory it will be also a required field in REST structure. For a better flexibility it is sufficient to make just those fields mandatory that are really qualifying an information.

02_CustomerPage

The starting point for creation of a Child Object is the Objects menu in AppComposer as shown in screenshot below. Choose Standard Objects and Accounts to create a custom structure for competitive intelligence being related to a customer record.

In our case we provide information for field definitions and access management as we must our service engineers able to enter information.

The new Child Objects exist  will run in a user specific sandbox, but can also published to the global sandbox once this solution will be made final. Why is the usage of sandboxes crucial for the development of extensions like this? As the name says a sandbox is an isolated runtime environment for every user subscribing to it. A user can subscribe to any sandbox, but not use more than one single sandbox at a time. Using sandboxes will decrease the risk of harming the entire system: if issues appear as a result of customizations and extensions they will run only in an isolated environment and not harm other business processes, UI’s or affect other functionalities. Only when a solution in a sandbox has been accurately tested and certified for a more common usage it should be published and made available globally as part of a careful maintenance activity. Its important to mention that all steps for registration of Child Objects and usage of their REST API’s are tied to a specific sandbox and only those users have a benefit of their usage who have applied to the specific sandbox where they have been created.

03_Child_Object

As shown in screenshot below we’ve created a record name containing the competitors name for our custom record.

04_ChildObject

For the record structure we’ve chosen the following fields

  • CompetitorInfo ⇒ Context information about the specific record – mandatory, but can be filled with CompetitorName if no other information is available
  • ProductNameInstalled ⇒ Name or Typ of our competitor’s product as been founders side
  • NumberOfItemsInstalled ⇒ whatever the service engineers see at customers site regarding installed competitors items
  • ProductInstallationDate ⇒ if known this field will hold the information about installation date
  • ProductExpirationDate ⇒ knowing the expiration date would be useful for our sales team to initiate our own upsell activity
  • ProductValue ⇒ if known we can enter the value of competitor items being sold to our customer

05_ChildObjectWe should bear in mind that our service engineers are not usually the audience working with core customer data. For a perfect fitting access management we should consider to create a special role for our service engineers and give them exactly the grant to manage competitive information they need.

06_ChildObjectOnce done, we’re good to collect the competitive information per account. This is available yet as shown in screenshot above. Means the members of sales team can start entering/maintaining the information. However the Sales Cloud UI might provide too many interactions for service engineers. For that audience we will create a more simplified UI to create information via the REST interface of our Child Object.

Child Object representation in Sales Cloud REST APIs

The REST structure of Sales Cloud objects is documented here. For a full introduction to Sales Cloud REST APIs refer to Arvind’s blog!

By using the URL https://<mysalescloud.oraclecloud.com>/crmCommonApi/resources/latest/accounts/describe we will retrieve information about the structure for Account REST API as shown below in Google Chrome’s extension Postman.

09_PostmanRestCallWhile the information above is probably known we might wonder how to address the custom Child Object for Competitive Information via REST. When editing the Child Objects in AppComposer we find the internal name set for the ChildCollection as “CompetitorCollection_c “ – this name is usually derived from Display Name of Child Object (“Competitor”) concatenated with “Collection_c”. The child structure in REST uses the same name concatenated with some context information: “OrganizationDEO_CompetitorCollection_c” as shown in screenshot below.

08_RestCompetitorsWhere do these context information come from? As a hint you might want to check the main page for our Child Object in AppComposer. There you will find “OrganizationProfile” as a parent object. Means the Child Objects are all linked to Organization Profile as additional information for this Standard Object. This link is fixed and cannot be changed to another node in the REST structure hierarchy.

As shown on top we received the entire Account structure in REST by using the “describe” qualifier as part of the URL. Our Child object description is embedded in that huge output, so that we have to search for it. Once found it looks like this as a declaration:

...
{
    "rel": "child",
    "href": "https://<mysalescloud.oraclecloud.com>:443/crmCommonApi/resources/11.1.11/accounts/{id}/child/OrganizationDEO_CompetitorCollection_c",
    "name": "OrganizationDEO_CompetitorCollection_c",
    "kind": "collection",
    "cardinality":
    {
        "value": "1 to *",
        "sourceAttributes": "OrganizationProfileId",
        "destinationAttributes": "OrganizationProfile_Id_c"
    }
},
...

Further down in REST structure we find more details about our child object – below shown for the Child Object structure and the field definition for “ProductNameInstalled” as a sample:

...
},
"children": {
...
    "OrganizationDEO_CompetitorCollection_c": 
    {
        "discrColumnType": false,
        "title": "Competitor",
        "titlePlural": "Competitors",
        "attributes": [
        {
            "name": "Id",
            "type": "integer",
            "updatable": false,
            "mandatory": true,
            "queryable": true,
            "allowChanges": "never",
            "precision": 32,
            "hasDefaultValueExpression": true,
            "title": "Record ID",
            "properties": 
            {
                "fnd:FND_AUDIT_ATTR_ENABLED": "false"
            }
        },
        {
                "name": "RowType",
                "type": "string",
        },
        ...
        {
            "name": "ProductNameInstalled_c",
            "type": "string",
            "updatable": true,
            "mandatory": false,
            "queryable": true,
            "allowChanges": "always",
            "precision": 80,
            "title": "Product Name Installed",
            "maxLength": "80",
            "properties": {
            "protectionObjectTitle": "Competitor",
            "fnd:OSN_ENABLED_ATTR": "true",
            "TOOLTIP": "Name of competitors product installed at our clients side",
            "protectionKey": "Competitor_c.ProductNameInstalled_c",
            "DISPLAYWIDTH": "50",
            "description": "Name of competitors product installed at our clients side",
            "protectionState": "TOKENIZED",
            "AttributeType": "Text",
            "ExtnCustom": "Y"
        }
    },
    ...

As the definition shows the information and fields for every specific Child Object are related to an {id} in the hierarchy. In our case for the Standard Object “Accounts” the unique key “PartyNumber” of parent object represents this ID. Means: without knowing the value of “PartyNumber” for a specific Account we can’t address the attached Child Object.

Using the REST interface for Child Objects to view/edit data

With the knowledge above about REST structures and addressing we will show in a sample how to retrieve and enter data for competitive information via REST.

One choice to evaluate the data would be Postman as a Google Chrome extension. As described in the standard Oracle Docs we can add parameters to our REST call to add some filter conditions. The screenshot below shows a sample where we look for a customer called “Willis Towers”. In the JSON result we find the value for PartyNumber (2nd item below): 34014. Now we got the information we need to access our child objects!

07_RestAccount

For those people who prefer a command line call to retrieve the data they could use curl with the following parameters:

curl -u <user>:<passwd> -H "Content-Type:application/json" \ 
-H "Accept: application/json" -k -X GET \ 
https://<mysalescloud.oraclecloud.com>/crmCommonApi/resources/latest/accounts?onlyData\&limit=200\&q=OrganizationName="Willis%20Towers"

The resulting JSON will look like this:

{ "items" : [ 
    { 
        "PartyId" : 300000007548087, 
        "PartyNumber" : "34014", 
        "SourceSystem" : null, 
        "SourceSystemReferenceValue" : null, 
        "OrganizationName" : "Willis Towers", 
        "UniqueNameSuffix" : null, 
        "PartyUniqueName" : "Willis Towers", 
        "Type" : "ZCA_PROSPECT", 
        "OwnerPartyId" : 300000006885963, 
        "OwnerPartyNumber" : "31005", 
        "OwnerEmailAddress" : "john.doe@foo.com", 
        "OwnerName" : "John Doe", 
        ...
}

Coming back to our sample for the service engineers having a custom application to collect competitive information we can follow the following algorithm as a sample:

  • Use an identifier as a filter for Accounts the service engineers are allowed to see and to manage in terms of competitive data
  • In our sample below we used the field OwnerName, but in real life cases any other filter condition would work the same or even better like a specific address information (customer location = “Chicago”) or a specific Organization or similar
  • The custom app will provide a choice list only for the customers the service engineers has a given permission via filter
  • We bear in mind that the service engineers job is the maintenance of our products and an entry of competitive intelligence must be straight-forward and quick – as said on top of this article ideally working as a mobile app on a smartphone

As shown in the Postman screenshot below we filter the PartyNumber and PartyUniqueName in our call. The name value will be shown in custom app later while the number will be used to address the Child Object.

10_PostmanRestCall

The call via curl would look like this:

curl -u <user>:<passwd> -H "Content-Type:application/json" -H "Accept: application/json" -k X GET \ 
https://<mysalescloud.oraclecloud.com>/crmCommonApi/resources/latest/acounts?onlyData\&limit=200\&q=OwnerName="Ulrich%20Janke"\&fields=PartyNumber,PartyUniqueName

Maybe worth to mention that we get an overview about number of records found, any limits for number or records in result set as defined in the REST call and information that more records exist:

{ 
    "items" : [ 
        { 
            "PartyId" : 300000007548087, 
            "PartyNumber" : "34014",
            ...
        } ], 
    "count" : 2, 
    "hasMore" : false, 
    "limit" : 200, 
    "offset" : 0, 
    "links" : [ 
        { 
            "rel" : "self", 
            "href" : "https://<mysalescloud.oraclecloud.com>/:443/crmCommonApi/resources/11.1.11/accounts", 
            "name" : "accounts", 
            "kind" : "collection" 
        } ]
...

It’s a business decision, but also a question of usability whether a long list of customers would make sense to be shown in custom UI. Apparently it would make rather sense to restrict it to a number less than 200 and to use any other filter condition. Solution details are not part of the scope in this article, but maybe a SR number or something similar would be better choice.

Once our service engineer has opened the custom app and entered the competitive intelligence data, the final action is to trigger a REST call to update the values of according Child Object by pressing the Submit button.

For this action two things are important to mention (also explained in detail in Oracle Docs):

  • We must used PATCH as operation to insert a new Child Object
  • The Content-Type must be “application/vnd.oracle.adf.resourceitem+json”

Screenshot below shows a sample REST call in Postman:

11_PostmanRestCall

12_PostmanRestCall

With curl the same REST call would look like this:

curl -u <user>:<passwd> -H "Content-Type: application/vnd.oracle.adf.resourceitem+json " \
 -H "Accept: application/json" -k X PATCH \
 -d '{\
         "RecordName":"Bad & Expensive",\
         "CompetitorName_c":"Deal late in 2014",\
         "ProductNameInstalled_c":"UJ/890-7896/ABC-890",\
         "NumberOfItemsInstalled_c":2, \
         "ProductInstallationDate_c":"2014-11-01",\
         "ProductExpirationDate_c":"2019-11-01"\
     }' \
 https://<mysalescloud.oraclecloud.com>/crmCommonApi/resources/latest/accounts/{partyID}/child/OrganizationDEO_CompetitorCollection_c

Once the call was executed successfully on server he will send back the complete new child record containing the competitive information.

Finally the screenshot below shows a sample UI as written in plain HTML and Javascript.

13_CompetitiveEntryApp

As mentioned in the beginning any JS framework including Oracle JET in combination with Oracle Mobile Cloud or Java Cloud Service would make sense to host such a custom app. The reason to provide a generic code sample is based on the consideration that we focus in this article about the logical structure to handle the access to Child Objects. A plain HTML sample is more compact and might be easier to read or to be used in an own test application other than some framework specific solution.

Below you can find the sample implementation in HTML:

<!DOCTYPE html>
<html>
<head>
    <title>Oracle Sales Cloud - Rest Access for Child Objects</title>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        form {
            width: 40em;
        }
 
        #h2 {
            font-weight: bold;
            font-size: 150%;
            width: 90%;
            margin-left: 30%;
            margin-bottom: 5%;
            height: 10%;
            float: left;
        }

        input,
        label.input {
            float: left;
            width: 40%;
        }
 
        select,
        select.option {
            width: 45%;
            margin-left: 30%;
        }
 
        select {
            margin: 0 0 1em .2em;
            padding: .2em .5em;
            background-color: #aaeee0;
            border: 1px solid #e7c157;
            font-weight: bold;
        }
 
        input {
            margin: 0 0 1em .2em;
            padding: .2em .5em;
            background-color: #fffbf0;
            border: 1px solid #e7c157;
        }
 
        label.input {
            text-align: right;
            line-height: 1.5;
            font-weight: bold;
        }
 
        label.input::after {
            content: ": ";
        }
 
        button {
            float: right;
            width: 30%;
        }
    </style>
</head>
<body>
    <main>
        <form id="competitiveInfo">
            <label id="h2" form="compInfo">Competitive Information</label>
            <label class="input" for="customerList">Customer</label>
            <select id="customerList" name="customerList"></select>
            <label class="input" for="compInfo">Competitor Name</label>
            <input type="text" id="compName" maxlength="100" required>
            <label class="input" for="compInfo">Additional Competitor Info</label>
            <input type="text" id="compInfo" maxlength="100">
            <label class="input" for="prodName">Product Name/Type</label>
            <input type="text" id="prodName" maxlength="100" required>
            <label class="input" for="numItems">Number of items</label>
            <input type="number" id="numItems" min="1" max="100" required> 
            <label class="input" for="installDate">Installation Date</label> 
            <input type="date" id="installDate"> 
            <label class="input" for="expireDate">Expiration Date</label> 
            <input type="date" id="expireDate"> 
            <button type="submit" id="mySubmit">Submit</button> 
            <button type="reset" id="myReset">Clear</button> 
        </form> 
    </main> 
    <script> 
        var handleSubmit = document.getElementById("mySubmit"); 
        var getCustData = null; 
        var sendCustData = null; 
        handleSubmit.addEventListener ('click', doSubmit); 
        document.addEventListener("DOMContentLoaded", doPastDocLoad);   

        function doPastDocLoad() { 
            var custDataURL = "https://<mysalescloud.oraclecloud.com>/crmCommonApi/resources/latest/accounts?onlyData&limit=200&q=OwnerName=John%20Doe&fields=PartyNumber,PartyUniqueName"; 
            getCustData = new XMLHttpRequest();   
            getCustData.onreadystatechange = processCustDataRequest; 
            getCustData.open( "GET", custDataURL, true ); 
            getCustData.setRequestHeader("content-type", "application/json"); 
            getCustData.setRequestHeader("accept", "application/json"); 
            getCustData.setRequestHeader("Allow-Control-Allow-Origin", "*" ); 
            getCustData.send( null ); 
        }   

        function processCustDataRequest() { 
            if ( getCustData.readyState === XMLHttpRequest.DONE && getCustData.status === 200 ) { 
                var myCusts = getCustData.responseText; 
                var custData = JSON.parse(myCusts);   
                
                if( custData.hasMore ) 
                    alert("More than " + custData.limit + " customers existing! Just showing first " + custData.count + " records ...");   

                var custDataOpts = '';   
               
                if( custData.count === 0 ) { 
                    custDataOpts = '<option value=0>NO CUSTOMER ASSIGNED</option>'; 
                    document.getElementById('customerList').innerHTML = custDataOpts; 
                    document.getElementById('mySubmit').setAttribute("disabled", "true"); 
                } 
                else { 
                    for (var i = 0; i < custData.count; i++) { 
                        custDataOpts += '<option value="'+ custData.items[i].PartyNumber + '">' + custData.items[i].PartyUniqueName + '</option>'; 
                    } 
                    document.getElementById('customerList').innerHTML = custDataOpts; 
                } 
            } 
        }
     
        function doSubmit() { 
            var custList = document.getElementById("customerList"); 
            var partyID = custList.options[custList.selectedIndex].value; 
            var compName = document.getElementById("compName").value; 
            var compInfo = document.getElementById("compInfo").value; 
            var prodName = document.getElementById("prodName").value; 
            var numItems = document.getElementById("numItems").value; 
            var installDate = document.getElementById("installDate").value; 
            var expireDate = document.getElementById("expireDate").value;
         
            if ( compInfo === '') 
                compInfo = compName;   
            
            var patchBodyString = '{ "RecordName": "' + compName + '",' + '"CompetitorName_c": "' + compInfo + '",' + '"ProductNameInstalled_c": "' + prodName + '",
                   ' + '"NumberOfItemsInstalled_c": ' + numItems; 
            
            if (installDate.toString() !== '') 
                patchBodyString = patchBodyString + ', "ProductInstallationDate_c": "' + installDate + '"'; 

            if (expireDate.toString() !== '') 
                patchBodyString = patchBodyString + ', "ProductExpirationDate_c": "' + expireDate + '"'; 

            patchBodyString = patchBodyString + ' }'; 
            var patchBody = JSON.parse(patchBodyString);   
            
            try { 
                sendCustData = new XMLHttpRequest(); 
                sendCustData.onreadystatechange = procCompUpdReq; 
                var updCustDataURL = "https://<mysalescloud.oraclecloud.com/crmCommonApi/resources/latest/accounts/" + partyID + "/child/OrganizationDEO_CompetitorCollection_c"; 
                sendCustData.open( "PATCH", updCustDataURL, true );   
                sendCustData.setRequestHeader("Content-Type", "application/vnd.oracle.adf.resourceitem+json"); 
                sendCustData.setRequestHeader("Allow-Control-Allow-Origin", "*" ); 
                sendCustData.send( patchBody ); 
            }   
        
            catch ( e ) { 
                if ( e instanceof TypeError ) 
                    console.log("TypeError occured!");   

                if ( e instanceof SecurityError ) 
                    console.log("SecurityError occured!");   

                if ( e instanceof InvalidAccessError) 
                    console.log("InvalidAccessError occured!");   

                console.log(e.message); 
                console.log(e.name); 
                console.log(e.fileName); 
                console.log(e.lineNumber); 
                console.log(e.columnNumber); 
                console.log(e.stack); 
                alert(“Failure: “ + e.message ); 
            } 
        }   
        
        function procCompUpdReq() { 
            if ( sendCustData.readyState === XMLHttpRequest.DONE ) { 
                var respText = sendCustData.responseText; 
                var respData = JSON.parse(respText);   

                if (sendCustData.status !== 200) { 
                    alert("Failed! Status= " + sendCustData.status + " readyState=" + sendCustData.readyState); 
                } 
                else { 
                    alert("Competitive Information successfully updated!"); 
                } 
            } 
        } 
    </script> 
</body> 
</html>

Troubleshooting

In this article I didn’t handle some topics as they were already mentioned in Angelo’s blog post about Oracle JET and Sales Cloud:

  • Security considerations and Login are well explained in that blog and also valid for the solution shown above. In my explanations above I didn’t provide any special instructions for user authentication and authorization. Please follow the instructions in Angelos blog.
  • When testing the custom app above from a development environment like Google Chrome you might run into some serious issues with CORS and pre-flight checks. By default OSC won’t answer OPTIONS calls that are sent in advance of a PATCH operation when testing from a browser. As a workaround you might follow the instructions in Angelo’s blog and deploy the code to a registered server.

Add Your Comment