This is a end to end Loyalty business scenario
MIT License
This repository will provision an environment that may be used as a Lab to build an end to end scenario that does the following:
This solution brings together Infrastructure as a Service (IaaS), Platform as a Service (PaaS), Software as a Service (SaaS) and Serverless components on Microsoft Azure to build a realistic end to end scenario to nurture customers. Furthermore, the democratization of AI is tied in nicely by incorporating Cognitive Services to perform text analysis and determine the sentiment of a customers feedback.
This solution will query a customer datastore, then get the last support case associated with a customer from a different datastore, check the sentiment/satisfaction of the customer's last feedback and generate a digital discount coupon for them if their sentiment is determined to be dissatisfied. The coupon will then be emailed to the customer to redeem.
The following technology components are used in this solution:
This solution will install and configure all of the components required to build the end to end Loyalty scenario. The Lab attendees just need to wire everything together in a Logic App.
For this Lab you will require:
Complete step 1 below prior to attending the lab
Select or create a Resource Group to deploy to and the only parameter you need to change is the Deployment Name - give it any name of 12 characters or less as it will be used to generate a hash to ensure your site names are unique, see the image below:
This will take roughly 30 minutes as this will provision:
This will navigate you to a URL that will display the following message if it was provisioned correctly:
Cannot GET /
Now type /docs
after the azurewebsites.net part of the url and you should see the Swagger editor open with a test harness to test the API:
You should now be able to test a few methods of the API to check if how it works. It will query 3 contacts and methods exist to query all, query by Id, query associated Case Number by Id and query contact Email by Id.
Now copy the URL without the /docs
component and paste is somewhere for retrieval later on.
Navigate to API Management component provisioned within Azure, its name will be generated by default with the following format cadapim[hash].
Click on the overview blade and select 'Publisher Portal' (note we will use the old portal while the new portal is still in preview), see below:
This will navigate you to the Publisher portal, select Import API, see below:
Now enter the following values:
/swagger
on the end e.g. http://cadapimastersite[hash].azurewebsites.net/swagger
See below:
You will now have imported an API that will now be accessible from the API Gateway. You can test it by clicking on Developer Portal, see below:
Click APIs --> Contact List API --> Try It
This will take you to the test harness of API Management. Click the eyeball and copy the value in the field Ocp-Apim-Subscription-Key, this is your APIMKey which we will use extensively, see below:
You can test the API now and it should return values with a status of 200 Ok.
We have now set up the first API in our process.
We could deploy this script as a custom script extension on the VM but that will complicate troubleshooting in a lab scenario so we will manually connect to the machine and run the build script, it is a single install script that will set up everything required.
Navigate to your VM, the default name will be CADLegacyAPI[hash] and navigate to the Overview blade and copy the value in the field Public IP Address/DNS label, see below:
We will now ssh onto the machine using Bash for Windows on Windows 10, or putty or just plain old terminal on a mac or Linux.
git clone https://github.com/shanepeckham/CADHackathon_Loyalty.git
cd CADHackathon_Loyalty
bash installVM.sh
Your Legacy Ticket API should now be listening on port 8000 but this will not be accessible from the outside world. Note, if you restart your VM or want to restart the legacy api, simply navigate to the /LegacyAPI/CADContacts folder and run node server.js. e.g.
cd CADContacts
node server.js
Navigate to API Management component provisioned within Azure, its name will be generated by default with the following format cadapim[hash].
Click on the overview blade and select 'Publisher Portal' (note we will use the old portal while the new portal is still in preview), see below:
This will navigate you to the Publisher portal, select Import API, see below:
Now enter the following values:
See below:
You will now have imported an API that will now be accessible from the API Gateway. You can test it by clicking on Developer Portal, see below:
Click APIs --> Contact Case List API --> Try It
This will take you to the test harness of API Management. Click the eyeball and copy the value in the field Ocp-Apim-Subscription-Key, this should be the same as your previous API product.
You can test the API now and it should return values with a status of 200 Ok.
We have now set up the legacy Ticket API in our process.
Click on the Deploy button below.
NB - Make sure you use the same Deployment Name as you did in Step 1.
Update: Azure functions has recently changed and the new release is not deploying the code correctly. Navigate to Platform Features --> Deployment Options, see below:
In the pane on the right click the 'Sync' button. This will redploy the code and will take around 2-3 minutes, see below:
Once complete, click the Refresh icon as highlighted below and you should see the following:
You can test the function in the test harness within the function app itself, add this to the Request body:
{ "name": "hello" }
You should see the output look something like this:
{"couponUrl":"https://cadfuncstorrvhyzok7zv4gw.blob.core.windows.net/coupons/%5Bobject%20Object%5D.jpg?st=2017-03-14T20%3A27%3A59Z&se=2017-03-14T21%3A27%3A59Z&sp=r&sv=2015-12-11&sr=b&sig=6UUHFHY08JihUU8vT%2Fus%2Fot9Pl%2BZud6jaakMNTuCFZc%3D"} Status 200 Ok
Note, if you get an error upon first invocation, run it again and it should work. You are now ready to build the logic app.
We want to get the customer's details, find their last associated case and then check the feedback against it.
See the diagram below for the simplistic data model to help you query the right data.
Create a HTTP Request Step, click save - you will receive an endpoint upon save.
Select the Request step, see below:
Once you save the Logic app you will get and endpoint, you can now invoke your logic app with Postman - add the URL and select POST. Ensure you have set the Header "Content-Type" with value "application/json". Select body, select "raw" and enter the follow value for your body content:
{
"APIMKey": "[Your APIM Key goes here]",
"id": 1
}
Now add a step to include an API Management API - select your API "Contact List API". You will want to select the method GET for contacts/{id}
You will need to navigate to the code view to be able to select the json fields that will be posted as part of the body. Note, you can select the values you are posting in the body using javascript object notation.
Your code view should look like this:
"QueryContactsById": {
"inputs": {
"api": {
"id": "/subscriptions/1b987fd6-b38e-40a1-bca8-4f67e6272c12/resourceGroups/[ResourceGroup]/providers/Microsoft.ApiManagement/service/cadapimxdb3o43h6p7bq/apis/58cd4c45dc78ac0f84da1287"
},
"method": "get",
"pathTemplate": {
"parameters": {
"id": "@{encodeURIComponent(triggerBody()['id'])}"
},
"template": "/Contacts/contacts/{id}"
},
"subscriptionKey": "@{triggerBody()['APIMKey']}"
},
"runAfter": {},
"type": "ApiManagement"
}
Navigating back to the designer should show your values resolved like below:
Now you need to query the Legacy Ticket API (Contacts Case List API) which is inside the isolated network to get the last case for that customer and retrieve the customer's feedback on the case. From the demo model hint above you want to use the caseNum field from the QueryContactsById to query the Legacy Ticket API field primary key field CaseId.
Add an API Management API step, select the Contact Case List API and once again query by Id, which in this CaseId which maps to the caseNum output from the previous step and add the API Management subscription key.
Ensure you select the correct outputs from the previous step(s) as inputs to this step, see below:
Now if you applied the logic from the previous step to select the correct outut field values (which would be a sensible approach) you will probably get the following error:
This is due to us outputting an array but not specifying which record/index we want to use. Change the following line from
"id": "@{encodeURIComponent(body('QueryContactsById')?['caseNum'])}"
to use the first value of the array, namely the 0 index:
"id": "@{encodeURIComponent(body('QueryContactsById')[0]['caseNum'])}"
Here is what your code view should look like for this step:
"QueryCasesById": {
"inputs": {
"api": {
"id": "/subscriptions/1b987fd6-b38e-40a1-bca8-4f67e6272c12/resourceGroups/[ResourceGroup]/providers/Microsoft.ApiManagement/service/cadapimxdb3o43h6p7bq/apis/58cd5516dc78ac0f84da1289"
},
"method": "get",
"pathTemplate": {
"parameters": {
"id": "@{encodeURIComponent(body('QueryContactsById')[0]['caseNum'])}"
},
"template": "/LegacyAPI/contacts/{id}"
},
"subscriptionKey": "@{triggerBody()['APIMKey']}"
},
"runAfter": {
"QueryContactsById": [
"Succeeded"
]
},
"type": "ApiManagement"
},
Now we want to add our Cognitive Services 'Detect Sentiment' (note you will need to have your key ready that you got when you signed up for the Text Analytics preview as part of the pre-requisites) step so that we can analyse the sentiment of the Ticket Feedback:
We now need to make sure we send the correct output from the QueryCasesById step to the Detect Sentiment step, use the javascript dot notation approach again with an index value. Your steps should look like this:
Your code view should look like this:
"Detect_Sentiment": {
"inputs": {
"body": {
"text": "@{body('QueryCasesById')?['last_feedback']}"
},
"host": {
"api": {
"runtimeUrl": "https://logic-apis-northeurope.azure-apim.net/apim/cognitiveservicestextanalytics"
},
"connection": {
"name": "@parameters('$connections')['cognitiveservicestextanalytics']['connectionId']"
}
},
"method": "post",
"path": "/sentiment"
},
"runAfter": {
"QueryCasesById": [
"Succeeded"
]
},
"type": "ApiConnection"
},
Now we want to add a condition to check the sentiment, if the probability outcome is less than 0.5, then it negative sentiment and therefore qualifies for our discount coupon.
Your condition should look like this:
With the following in code view:
"expression": "@less(body('Detect_Sentiment')?['score'], 0.5)",
"runAfter": {
"Detect_Sentiment": [
"Succeeded"
]
},
"type": "If"
Now you can call the GenerateCoupon function if the condition is met, pass in the name of the user that you want to generate the digital coupon for:
With the following in the code view:
"GenerateCoupon": {
"inputs": {
"body": {
"name": "@body('QueryContactsById')[0]['name']"
},
"function": {
"id": "/subscriptions/1b987fd6-b38e-40a1-bca8-4f67e6272c12/resourceGroups/NewLoyaltyPlan/providers/Microsoft.Web/sites/CADFuncxdb3o43h6p7bq/functions/GenerateCoupon"
}
},
"runAfter": {},
"type": "Function"
}
},
Now we want to send an email to every receipient to inform them that they can download a digital coupon which we have generated for them.
Your email step should look like this:
With the following code view:
"Send_email": {
"inputs": {
"body": {
"Body": "Please accept this coupon: @{body('GenerateCoupon')}",
"Subject": "We heard that you were not happy",
"To": "@{body('QueryContactsById')[0]['email']}"
},
"host": {
"api": {
"runtimeUrl": "https://logic-apis-northeurope.azure-apim.net/apim/gmail"
},
"connection": {
"name": "@parameters('$connections')['gmail']['connectionId']"
}
},
"method": "post",
"path": "/Mail"
},
"runAfter": {
"GenerateCoupon": [
"Succeeded"
]
},
"type": "ApiConnection"
}
The full code solution view looks like this:
{
"$connections": {
"value": {
"cognitiveservicestextanalytics": {
"connectionId": "/subscriptions/1b987fd6-b38e-40a1-bca8-4f67e6272c12/resourceGroups/NewLoyaltyPlan/providers/Microsoft.Web/connections/cognitiveservicestextanalytics",
"connectionName": "cognitiveservicestextanalytics",
"id": "/subscriptions/1b987fd6-b38e-40a1-bca8-4f67e6272c12/providers/Microsoft.Web/locations/northeurope/managedApis/cognitiveservicestextanalytics"
},
"gmail": {
"connectionId": "/subscriptions/1b987fd6-b38e-40a1-bca8-4f67e6272c12/resourceGroups/NewLoyaltyPlan/providers/Microsoft.Web/connections/gmail",
"connectionName": "gmail",
"id": "/subscriptions/1b987fd6-b38e-40a1-bca8-4f67e6272c12/providers/Microsoft.Web/locations/northeurope/managedApis/gmail"
}
}
},
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"actions": {
"Condition": {
"actions": {
"GenerateCoupon": {
"inputs": {
"body": {
"name": "@body('QueryContactsById')[0]['name']"
},
"function": {
"id": "/subscriptions/1b987fd6-b38e-40a1-bca8-4f67e6272c12/resourceGroups/NewLoyaltyPlan/providers/Microsoft.Web/sites/CADFuncxdb3o43h6p7bq/functions/GenerateCoupon"
}
},
"runAfter": {},
"type": "Function"
},
"Send_email": {
"inputs": {
"body": {
"Body": "Please accept this coupon: @{body('GenerateCoupon')}",
"Subject": "We heard that you were not happy",
"To": "@{body('QueryContactsById')[0]['email']}"
},
"host": {
"api": {
"runtimeUrl": "https://logic-apis-northeurope.azure-apim.net/apim/gmail"
},
"connection": {
"name": "@parameters('$connections')['gmail']['connectionId']"
}
},
"method": "post",
"path": "/Mail"
},
"runAfter": {
"GenerateCoupon": [
"Succeeded"
]
},
"type": "ApiConnection"
}
},
"expression": "@less(body('Detect_Sentiment')?['score'], 0.5)",
"runAfter": {
"Detect_Sentiment": [
"Succeeded"
]
},
"type": "If"
},
"Detect_Sentiment": {
"inputs": {
"body": {
"text": "@{body('QueryCasesById')?['last_feedback']}"
},
"host": {
"api": {
"runtimeUrl": "https://logic-apis-northeurope.azure-apim.net/apim/cognitiveservicestextanalytics"
},
"connection": {
"name": "@parameters('$connections')['cognitiveservicestextanalytics']['connectionId']"
}
},
"method": "post",
"path": "/sentiment"
},
"runAfter": {
"QueryCasesById": [
"Succeeded"
]
},
"type": "ApiConnection"
},
"QueryCasesById": {
"inputs": {
"api": {
"id": "/subscriptions/1b987fd6-b38e-40a1-bca8-4f67e6272c12/resourceGroups/NewLoyaltyPlan/providers/Microsoft.ApiManagement/service/cadapimxdb3o43h6p7bq/apis/58cd5516dc78ac0f84da1289"
},
"method": "get",
"pathTemplate": {
"parameters": {
"id": "@{encodeURIComponent(body('QueryContactsById')[0]['caseNum'])}"
},
"template": "/LegacyAPI/contacts/{id}"
},
"subscriptionKey": "@{triggerBody()['APIMKey']}"
},
"runAfter": {
"QueryContactsById": [
"Succeeded"
]
},
"type": "ApiManagement"
},
"QueryContactsById": {
"inputs": {
"api": {
"id": "/subscriptions/1b987fd6-b38e-40a1-bca8-4f67e6272c12/resourceGroups/NewLoyaltyPlan/providers/Microsoft.ApiManagement/service/cadapimxdb3o43h6p7bq/apis/58cd4c45dc78ac0f84da1287"
},
"method": "get",
"pathTemplate": {
"parameters": {
"id": "@{encodeURIComponent(triggerBody()['id'])}"
},
"template": "/Contacts/contacts/{id}"
},
"subscriptionKey": "@{triggerBody()['APIMKey']}"
},
"runAfter": {},
"type": "ApiManagement"
}
},
"contentVersion": "1.0.0.0",
"outputs": {},
"parameters": {
"$connections": {
"defaultValue": {},
"type": "Object"
}
},
"triggers": {
"manual": {
"inputs": {
"schema": {}
},
"kind": "Http",
"type": "Request"
}
}
}
}
If you get and 'Internal Server Error' 500 on the Contact Case List (Legacy Ticket API) this could be because the node API has stopped. ssh into the VM and navigate to the /LegacyAPI/CADContacts folder and run 'node server.js'. e.g.
cd CADContacts
nohup node server.js &
The logout out of the ssh session.
If you get a 404 - Resource not found within the Logic app invoking the Legacy API, ensure that you have selected HTTPS not HTTP as the Web API URL Scheme. Check your settings are the same as in this image below:
The security lab for this environment may be found here Securing the Loyalty environment