Skip to Content
Technical Articles
Author's profile photo Kunj Bihari Shukla

SAP S/4HANA integration with SAP Conversational AI Chatbot using MS Teams – Part 3

Hi Experts,

Hope all is going well!

Quick Overview

Krishna Chaitanya & Myself has been working on SAP Conversational AI Chatbots for a while and want to share one of the interesting use case in which, we will do an end to end integration from the S/4 Hana system to SAP Conversational AI Chatbot and then access the chatbot from the MS Teams.

This blog post is the 4th part of the blog series SAP SAP S/4HANA integration with SAP Conversational AI Chatbot using MS Teams.

  1. SAP S/4HANA integration with SAP Conversational AI Chatbot using MS Teams – Part 1 ,
  2. SAP S/4HANA integration with SAP Conversational AI Chatbot using MS Teams – Part 2 ,
  3. SAP S/4HANA integration with SAP Conversational AI Chatbot using MS Teams – Part 3  and
  4. SAP S/4HANA integration with SAP Conversational AI Chatbot using MS Teams – Part 4

Let’s have a quick recap of what we have already achieved in this blog series:

  • Activated/created the OData service in the S/4 Hana onPrem system
  • Exposed the OData service to the SAP BTP trial account via the SAP Cloud connector.

Since SAP Conservational AI is not offered as a service in SAP BTP trial account hence it can’t consume the OData or the Destination Service directly into the SAP CAI chatbot — Community Version. Seems like a problem, isn’t it??

Now in order to overcome this, Lets create a nodeJS application in our local system and deploy it to the cloud foundry(SAP BTP). Once deployed the nodeJS application should access the destinations which in-turn can provide access to the S/4 Hana OData services. NodeJS application will use the incoming data from the created Destinations and expose a public service API.

Agenda:

  1. Create Destinations — Using this, OData services can be accessed in SAP BTP applications.
  2. Create Connectivity and Destination instance — It is a necessary step to access OData services in SAP BTP applications.
  3. Create a nodeJS application in the local system — to consume the OData service and modify it as per our needs
  4. Deploy the nodeJS application to the SAP BTP using CLI  — once deployed the application can consume the OData service and expose it as a public service API
  5. Test the service — Now hard part is over, let’s test whether its working

Prerequisite:

  1. Access to SAP BTP Trial account
  2. Visual studio should be preinstalled in the local system
  3. nodeJS should be preinstalled with basic understanding of nodeJS.
  4. All the necessary plugins for cloud foundry CLI should be preinstalled in the local system. Check the link for more details on the same.

 

Step 1: Create Destinations

The Destination service lets you find the destination information that is required to access a remote service or system from your Cloud Foundry application. Below fields are relevant while creating the destination:

  • Name: Name of the Destination, can be any text.
  • Type: Select HTTP
  • Description: Short Description about the destination. Can be any text.
  • URL: Service URL forthe service to be exposed e.g. For purchase order the value should be http://HOST:PORT/sap/opu/odata/sap/MM_PUR_PO_MAINT_V2_SRV
  • Proxy Type: Select OnPremise
  • Authentication: Select Basic Authentication
  • Location ID: Can be any text.
  • User: User ID as in the onPrem system
  • Password: Password as in the onPrem system for the above user

Properties should be set as:

  • HTML5.DynamicDestination: true
  • HTML5.Timeout: 12000
  • sap-client: provide onPrem system client
  • WebIDEEnabled: true
  • WebIDESystem: provide onPrem system ID
  • WebIDEUsage: odata_gen,odata_abap,dev_abap

Please find the below screenshot on precise data to be entered to create Destination for Purchase requisition, Purchase Order and Sales order respectively.

Step 2: Create Connectivity and Destination instance

To consume the Destinations created in step 1 from the nodeJS application, Connectivity and Destination instance needs to be created and should be bound to the nodeJS application.

For the connection to an on-premise system, Connectivity service should be used together with (i.e. in addition to) the Destination service.

To consume the Connectivity service from an application, you must create a service instance and bind it to the application.

Step 3: Create nodeJS App:

Lets get started with the nodeJS application. In this application the connectivity and destination services will be used to fetch the data from the back-end S/4 Hana system. Using the configured destinations the data will be fetched from OData Services activated earlier. Data thus received can be modified as per the requirement and exposed as a public API. 

  1. Create a folder “botNodeJS” in the workspace of the visual studio
  2. Open the terminal and navigate to the folder botNodeJS and run “npm init”Click enter to finally create a nodeJS project “botnodejs”
  3. Go Inside folder “botNodeJS” and create the below files if not already there
    1. manifest.yml
    2. package.json — auto created on running npm init
    3. server.js
  4. Once done your project structure will look as below
  5. manifest.yml: Copy the below code in the manifest.yml and save it.
    ---
    applications:
    - name: botnodejs
      # command: node server.js
      memory: 128M
      buildpacks:
      - nodejs_buildpack
      services:
      - connectivity-bot-01
      - destination-bot-01
  6. package.json: Append the below code in package.json and save it.
    {
        "dependencies": {
          "axios": "^0.21.0",
          "express": "^4.17.1"
        }
    }
  7. server.js: Add the below code to server.js and save it. To understand the logic inside each function.
    //Load libraries
    const axios = require('axios')
    const express = require('express')
    var url = require('url')
    const app = express()
    
    var queryParam;
    //to get data from VCAP_SERVICES:: Applications running in Cloud Foundry gain access 
    //to the bound service instances via credentials stored in an environment variable called VCAP_SERVICES.
    const VCAP_SERVICES = JSON.parse(process.env.VCAP_SERVICES);
    const destSrvCred = VCAP_SERVICES.destination[0].credentials;
    const conSrvCred = VCAP_SERVICES.connectivity[0].credentials;
    
    app.listen(process.env.PORT, function () {
        console.log('botNodeJS application started')
    })
    
    //to fetch auth token using URL, client and secret values
    const _fetchJwtToken = async function (oauthUrl, oauthClient, oauthSecret) {
        return new Promise((resolve, reject) => {
            //prepare URL
            const tokenUrl = oauthUrl + '/oauth/token?grant_type=client_credentials&response_type=token'
            //prepare for the call
            const config = {
                headers: {
                    Authorization: "Basic " + Buffer.from(oauthClient + ':' + oauthSecret).toString("base64")
                }
            }
            //backend get call to fetch auth token
            axios.get(tokenUrl, config)
                .then(response => {
                    resolve(response.data.access_token)
                })
                .catch(error => {
                    reject(error)
                })
        })
    }
    
    // Reads Destination configuration based on destinationName, destUri(fetched from VCAP_SERVICES) 
    // and jwtToken(fetched from _fetchJwtToken) . Result will be an object with Destination Configuration info 
    const _readDestinationConfig = async function (destinationName, destUri, jwtToken) {
        return new Promise((resolve, reject) => {
            //prepare URL
            const destSrvUrl = destUri + '/destination-configuration/v1/destinations/' + destinationName
            // preparation for the call
            const config = {
                headers: {
                    Authorization: 'Bearer ' + jwtToken
                }
            }
            //backend get call to fetch destination config
            axios.get(destSrvUrl, config)
                .then(response => {
                    resolve(response.data.destinationConfiguration)
                })
                .catch(error => {
                    reject(error)
                })
        })
    }
    //podetails: entity using which application exposes the PO Data
    app.get('/podetails', async function (req, res) {
        // call destination service //
        //fetch detination auth token
        const destJwtToken = await _fetchJwtToken(destSrvCred.url, destSrvCred.clientid, destSrvCred.clientsecret)
        //read destination config
        const destiConfi = await _readDestinationConfig('S4_purchase_order', destSrvCred.uri, destJwtToken)
        //to fetch query parameter from URL
        queryParam = url.parse(req.url, true).query;
    
        // call onPrem/Remote system using the connectivity service via the Cloud Connector//
        // fetch connectivity auth token
        const connJwtToken = await _fetchJwtToken(conSrvCred.token_service_url, conSrvCred.clientid, conSrvCred.clientsecret)
        try {
            // method to make a call to onPrem/Remote system, and save the result in variable "result"
            const result = await _poDetails(conSrvCred.onpremise_proxy_host, conSrvCred.onpremise_proxy_http_port, connJwtToken, destiConfi)
            res.json(result);
        }
        //catch block to handle any errors
        catch (e) {
            console.log('Catch an error: ', e)
            res.json({ "d": { "error": "error" } })
        }
    })
    //to make a backend call to the onPrejm/Remote system using connProxyHost, connProxyPort, ConnJwtToken (fetched 
    //using the connectivity service) and destiConfi (destination configuration fetched using destination service)
    const _poDetails = async function (connProxyHost, connProxyPort, connJwtToken, destiConfi) {
        return new Promise((resolve, reject) => {
            // make target URL 
            const targetUrl = destiConfi.URL + "/C_PurchaseOrderTP(PurchaseOrder='" + queryParam.number + "',DraftUUID=guid'00000000-0000-0000-0000-000000000000',IsActiveEntity=true)"
            //encode user creds fetched from the destination configuration
            const encodedUser = Buffer.from(destiConfi.User + ':' + destiConfi.Password).toString("base64")
            //preparation for the  onPrem/Remote  system call
            const config = {
                headers: {
                    Authorization: "Basic " + encodedUser,
                    'Proxy-Authorization': 'Bearer ' + connJwtToken,
                    'SAP-Connectivity-SCC-Location_ID': destiConfi.CloudConnectorLocationId
                },
                proxy: {
                    host: connProxyHost,
                    port: connProxyPort
                }
            }
            // get call to the onPrem/Remote system to fetch data
            axios.get(targetUrl, config)
                .then(response => {
                    resolve(response.data)
                })
                .catch(error => {
                    reject(error)
                })
        })
    }
    //prdetails: entity using which application exposes the PR Data
    app.get('/prdetails', async function (req, res) {
        // call destination service 
        //fetch detination auth token
        const destJwtToken = await _fetchJwtToken(destSrvCred.url, destSrvCred.clientid, destSrvCred.clientsecret)
        //read destination config
        const destiConfi = await _readDestinationConfig('S4_purchase_req', destSrvCred.uri, destJwtToken)
        //to fetch query parameter from URL
        queryParam = url.parse(req.url, true).query;
    
        // call onPrem/Remote system using the connectivity service via the Cloud Connector//
        // fetch connectivity auth token
        const connJwtToken = await _fetchJwtToken(conSrvCred.token_service_url, conSrvCred.clientid, conSrvCred.clientsecret)
        try {
            // method to make a call to onPrem/Remote system, and save the result in variable "result"
            const result = await _prDetails(conSrvCred.onpremise_proxy_host, conSrvCred.onpremise_proxy_http_port, connJwtToken, destiConfi)
            res.json(result);
        }
        //catch block to handle any errors
        catch (e) {
            console.log('Catch an error: ', e)
            res.json({ "d": { "error": "error" } })
        }
    })
    //to make a backend call to the onPrem/Remote system using connProxyHost, connProxyPort, ConnJwtToken (fetched 
    //using the connectivity service) and destiConfi (destination configuration fetched using destination service)
    const _prDetails = async function (connProxyHost, connProxyPort, connJwtToken, destiConfi) {
        return new Promise((resolve, reject) => {
            // make target URL 
            const targetUrl = destiConfi.URL + "/C_PurchaseReqnHeader(PurchaseRequisition='" + queryParam.number + "',DraftUUID=guid'00000000-0000-0000-0000-000000000000',IsActiveEntity=true)"
            //encode user creds fetched from the destination configuration
            const encodedUser = Buffer.from(destiConfi.User + ':' + destiConfi.Password).toString("base64")
            //preparation for the  onPrem/Remote  system call
            const config = {
                headers: {
                    Authorization: "Basic " + encodedUser,
                    'Proxy-Authorization': 'Bearer ' + connJwtToken,
                    'SAP-Connectivity-SCC-Location_ID': destiConfi.CloudConnectorLocationId
                },
                proxy: {
                    host: connProxyHost,
                    port: connProxyPort
                }
            }
            // get call to the onPrem/Remote system to fetch data
            axios.get(targetUrl, config)
                .then(response => {
                    resolve(response.data)
                })
                .catch(error => {
                    reject(error)
                })
        })
    
    }
    //sodetails: entity using which application exposes the SO Data
    app.get('/sodetails', async function (req, res) {
        var destinationNames = 'S4_sales_order';
        // call destination service //
        //fetch detination auth token
        const destJwtToken = await _fetchJwtToken(destSrvCred.url, destSrvCred.clientid, destSrvCred.clientsecret)
        //read destination config
        const destiConfi = await _readDestinationConfig( destinationNames, destSrvCred.uri, destJwtToken)
        //to fetch query parameter from URL
        queryParam = url.parse(req.url, true).query;
    
        // call onPrem/Remote system using the connectivity service via the Cloud Connector//
        // fetch connectivity auth token
        const connJwtToken = await _fetchJwtToken(conSrvCred.token_service_url, conSrvCred.clientid, conSrvCred.clientsecret)
        try {
            // method to make a call to onPrem/Remote system, and save the result in variable "result"
            const result = await _soDetails(conSrvCred.onpremise_proxy_host, conSrvCred.onpremise_proxy_http_port, connJwtToken, destiConfi)
            res.json(result);
        }
        //catch block to handle any errors
        catch (e) {
            console.log('Catch an error: ', e)
            res.json({ "d": { "error": "error" } })
        }
    })
    //to make a backend call to the onPrejm/Remote system using connProxyHost, connProxyPort, ConnJwtToken (fetched 
    //using the connectivity service) and destiConfi (destination configuration fetched using destination service)
    const _soDetails = async function (connProxyHost, connProxyPort, connJwtToken, destiConfi) {
        return new Promise((resolve, reject) => {
            // make target URL 
            const targetUrl = destiConfi.URL + "/zabibot01('" + queryParam.number + "')"
            //encode user creds fetched from the destination configuration
            const encodedUser = Buffer.from(destiConfi.User + ':' + destiConfi.Password).toString("base64")
            //preparation for the  onPrem/Remote  system call
            const config = {
                headers: {
                    Authorization: "Basic " + encodedUser,
                    'Proxy-Authorization': 'Bearer ' + connJwtToken,
                    'SAP-Connectivity-SCC-Location_ID': destiConfi.CloudConnectorLocationId
                },
                proxy: {
                    host: connProxyHost,
                    port: connProxyPort
                }
            }
            // get call to the onPrem/Remote system to fetch data
            axios.get(targetUrl, config)
                .then(response => {
                    resolve(response.data)
                })
                .catch(error => {
                    reject(error)
                })
        })
    }
    
  8. Open the integrated terminal in visual studio and navigate to the “botnodejs” folder and execute “npm install”

Step 4: Lets deploy the app to the SAP BTP trial account

    1. Open the terminal in the Visual studio and navigate to botNodeJS folder.
    2. Execute “cf login” and provide the sap btp trial account credentials to login to the SAP BTP.
    3. Use command “cf push” to deploy the nodeJS app to the sap BTP trial account. Once succesfully deployed, message similar to the below will be displayed on the terminal.
    4. After successful completion of the deployment, the “botnodejs” app can be seen in the SAP BTP trial account. Make sure the state of the application is started.
    5. Click on the highlighted link with text botnodejs to display the application overview. In this section check the application routes s

Step 5: Lets test our service API for data

 

We are now at the end of this blog post. During the course of this blog post, successfully created Destinations, created Connectivity & Destination instance. Once done, created a nodeJS application in the local system and deployed the nodeJS application to the SAP BTP using CLI. Once all is done we have a public service API.

In the next blog post we will create a SAP CAI chatbot, integrate it to the service API created by the nodeJS application so that we can have a working chatbot. Once done, integrate the chatbot with MS Teams so that users can easily access the chatbot during their day to day work.

Feedback :

Thanks for taking your time to go through this article, we hope you liked the content. We would appreciate, if you can spare few minutes to leave us feedback on your experience.

See you in the next part of this blog post 🙂 🙂

Assigned Tags

      2 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Rakesh Kumar
      Rakesh Kumar

      Hi Kunj/Krishna,

      Thanks! for the detailed blog. I was trying to access 4th part using link given in content, but its not working. Can you check that.

      Looking forward to next part.

      /Rakesh

      Author's profile photo Nirmala S
      Nirmala S

      please share part4