- Twillio_Config__c
- Endpoint__c // get from learn section in SMS -> Whatsapp Sandbox // Eg. https://api.twilio.com/2010-04-01/Accounts/ACc3ecc80bfb52e43e0beee178c0bc4862/Messages.json
- Account_SID__c // fetch from twilio dashboard
- Auth_Token__c // fetch from twilio dashboard
Last active
May 19, 2020 15:53
-
-
Save swapnilshrikhande/25af606b06ef6cf07dc2d84fa0aa9b85 to your computer and use it in GitHub Desktop.
Twilio Salesforce Simple Chat Bot Integration WARNING : Hardcoded data & No Error Handling
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
without sharing public class CaseCreationService { | |
//Queries | |
//How do we autoselect the product ? chat ? | |
//If no contact found should we create one ? | |
//How do we link the product | |
private final Case caseRecord = new Case(); | |
private String contactName; | |
public static final String MESSAGE_BODY='Body'; | |
public static final String MESSAGE_FROM='From'; | |
public static final String MESSAGE_TO='To'; | |
public CaseCreationService defaultFields(){ | |
caseRecord.Subject = 'Faulty Part Replacement Request'; | |
caseRecord.Reason = 'Faulty Part'; | |
caseRecord.Origin = 'Message'; | |
return this; | |
} | |
public static String getPersonName(RestRequest req){ | |
return new CaseCreationService().populateFromBio(extractNumber( | |
req.params.get(MESSAGE_FROM) | |
)).getContactName(); | |
} | |
public CaseCreationService populateFromBio(String phoneNumb){ | |
Id contactId; | |
//fetch contactid and account id from the phone number | |
phoneNumb = phoneNumb.trim(); | |
System.debug('Searching for phone='+phoneNumb); | |
List<Contact> contactLst = [ | |
SELECT AccountId | |
, Id | |
, Name | |
From Contact | |
where Phone = :phoneNumb | |
OR MobilePhone = :phoneNumb | |
limit 1 | |
]; | |
System.debug('Searching for phone='+contactLst); | |
if( !contactLst.isEmpty() ) { | |
caseRecord.ContactId = contactLst[0].Id; | |
caseRecord.AccountId = contactLst[0].AccountId; | |
contactName = contactLst[0].Name; | |
} | |
return this; | |
} | |
public CaseCreationService save(){ | |
upsert caseRecord; | |
return this; | |
} | |
//Request Parsing : can be another service | |
public CaseCreationService parseRestRequest(RestRequest req){ | |
this.defaultFields() | |
.parseToCase(req.params) | |
.populateFromBio(extractNumber( | |
req.params.get(MESSAGE_FROM) | |
)); | |
return this; | |
} | |
public CaseCreationService parseToCase(Map<String,String> requestMap){ | |
//'To'; 'whatsapp:+14155238886' | |
//'From'; 'whatsapp:+919860337398' | |
//'Body'; body text | |
//To | |
caseRecord.Service_Contact_Phone__c = extractNumber( | |
requestMap.get(MESSAGE_TO) | |
); | |
//From | |
caseRecord.SuppliedPhone = extractNumber( | |
requestMap.get(MESSAGE_FROM) | |
); | |
caseRecord.Description = requestMap.get(MESSAGE_BODY); | |
return this; | |
} | |
public CaseCreationService setProductId(String productId){ | |
caseRecord.ProductId = productId; | |
return this; | |
} | |
//getters | |
public String getCaseNumber(){ | |
if( caseRecord.Id == null ) | |
return null; | |
String caseNumber = [ | |
Select CaseNumber | |
From Case | |
Where Id = :caseRecord.Id | |
limit 1].CaseNumber; | |
return caseNumber; | |
} | |
public Case getCase(){ | |
return caseRecord; | |
} | |
public String getContactName(){ | |
return String.isEmpty(contactName) ? 'User' : contactName; | |
} | |
//utils | |
public static String extractNumber(String phoneString){ | |
return phoneString.split(':')[1].trim(); | |
} | |
public static String getSelectedProduct(RestRequest req){ | |
String phoneNumb = extractNumber(req.params.get(MESSAGE_FROM)); | |
List<Case> caseList = [ | |
Select ProductId | |
from Case | |
Where SuppliedPhone =:phoneNumb | |
AND Reason = 'Faulty Part' | |
AND Origin = 'Message' | |
ORDER BY CreatedDate desc | |
limit 1 | |
]; | |
if(!caseList.isEmpty()) | |
return caseList[0].ProductId; | |
return ''; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*Webhook to receive message inside Salesforce*/ | |
@RestResource(urlMapping='/pushmessage/*') | |
global with sharing class IncomingWhatsAppService { | |
//@TODO Move to custom label | |
public static final Map<String,String> productMap = new Map<String,String>{ | |
'1' => 'Engine Mount - EM-08921', | |
'2' => 'Front Forks - FF-8462', | |
'3' => 'Hose - H-05946', | |
'4' => 'Hydromounts - H-6742', | |
'5' => 'Slip Joints - SJ-6751', | |
'6' => 'All Products' | |
}; | |
public static final Map<String,String> productIdMap = new Map<String,String>{ | |
'1' => '01t5w000008POqPAAW', | |
'2' => '01t5w000008POqVAAW', | |
'3' => '01t5w000008POqUAAW', | |
'4' => '01t5w000008POqeAAG', | |
'5' => '01t5w000008POqZAAW', | |
'6' => 'All Products' | |
}; | |
public static final String YES = 'yes'; | |
public static final String NO = 'no'; | |
public static final String ERROR = 'Error:'; | |
public static final String CASELOG_SUCCESSMSG = ' A new Case *{0}* has been logged for your request.\n' | |
+'We will place an order for the part *{1}* to be replaced on your behalf once you confirm.\n' | |
+'Please respond with a *yes* or *no* to continue placing the order.'; | |
public static final String ORDER_PLACED_YESMSG = 'Your order *{0}* has been successfully placed and submitted for approval.\n' | |
+'Once approved, you will be notified of the invoice and shipment details of the part shortly.'; | |
public static final String ORDER_PLACED_NOMSG = 'Thank you for your response. You will need to place an order through our dealer portal *https://sforce.co/36fti5x* to request a replacement for the part under consideration.'; | |
public static final Map<String,String> orderConfirmMap = new Map<String,String>{ | |
YES => ORDER_PLACED_YESMSG, | |
NO => ORDER_PLACED_NOMSG | |
}; | |
public static final String ServiceMessageResponseTemplate = | |
'Hi {0}, Please select a product from below list to place a replacement order.\n' | |
+' 1. Engine Mount - EM-08921\n' | |
+' 2. Front Forks - FF-8462\n' | |
+' 3. Hose - H-05946\n' | |
+' 4. Hydromounts - H-6742\n' | |
+' 5. Slip Joints - SJ-6751\n' | |
+' 6. All Products\n' | |
+'Reply with the product option number.'; | |
@HttpGet | |
global static String doGet() { | |
return 'hello world'; | |
} | |
@HttpPost | |
global static void doPost() { | |
RestRequest req = RestContext.request; | |
RestResponse res = Restcontext.response; | |
Utils.debugMap('Header',req.headers); | |
Utils.debugMap('Param',req.params); | |
//Routing | |
//Get Message Body | |
String messageBody = req.params.get(CaseCreationService.MESSAGE_BODY); | |
messageBody = Utils.sanitize(messageBody); | |
Boolean isProductSelectResponse = productMap.containsKey(messageBody); | |
Boolean isOrderConfirmResponse = orderConfirmMap.containsKey(messageBody); | |
//If cookie is blank then first request | |
//Send Product Options | |
if( isProductSelectResponse == false && isOrderConfirmResponse == false ) { | |
sendProductOptions(req,res); | |
} else if( isProductSelectResponse ) { | |
sendOrderConfirmation(req,res,messageBody); | |
} else if( isOrderConfirmResponse ) { | |
sendOrderDetails(req,res, messageBody ); | |
} | |
return; | |
} | |
private static void sendProductOptions(RestRequest req,RestResponse res){ | |
String contactName = CaseCreationService.getPersonName(req); | |
//generating response | |
res.addHeader('Content-Type', 'text/plain'); | |
res.headers.put('content-type','text/plain'); | |
String responseStr = String.format(ServiceMessageResponseTemplate, new List<String>{contactName}); | |
res.responseBody = Blob.valueOf(responseStr); | |
} | |
private static void sendOrderConfirmation(RestRequest req,RestResponse res,String msg){ | |
String caseNumber = createCase(req,res,msg); | |
res.addHeader('Content-Type', 'text/plain'); | |
String responseStr = String.format( | |
CASELOG_SUCCESSMSG | |
, new List<String>{ caseNumber, productMap.get(msg) } ); | |
res.responseBody = Blob.valueOf(responseStr); | |
} | |
private static String createCase(RestRequest req,RestResponse res,String msg){ | |
//create case record | |
CaseCreationService caseCreator = new CaseCreationService() | |
.parseRestRequest(req) | |
.setProductId( productIdMap.get(msg) ) | |
.save(); | |
return caseCreator.getCaseNumber(); | |
} | |
private static void sendOrderDetails(RestRequest req,RestResponse res,String message){ | |
//get the product Id from last case created for the incoming number | |
String productId = CaseCreationService.getSelectedProduct(req); | |
res.addHeader('Content-Type', 'text/plain'); | |
if( message.equalsIgnoreCase(YES) ) { | |
String orderName = OrderCreationService.createOrder( productId ); | |
String response; | |
if( orderName.startsWith(ERROR) ){ | |
response = orderName; | |
} else { | |
response = String.format(orderConfirmMap.get(message), new List<String>{orderName}); | |
} | |
res.responseBody = Blob.valueOf( response ); | |
} else { | |
res.responseBody = Blob.valueOf( orderConfirmMap.get(message) ); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
without sharing public class OrderCreationService { | |
public static String createOrder(String productId){ | |
PriceBook2 pb2Standard = [select Id from Pricebook2 where isStandard=true]; | |
Id standardPriceBookId = pb2Standard.Id; | |
//Create Order Record | |
//Hardcoded value for Demo purpose | |
Order orderRecord = new Order(); | |
orderRecord.AccountId = '0015w00002Br136AAB'; | |
orderRecord.ContractId = '8005w000001Eb3GAAS'; | |
orderRecord.Status = 'Draft'; | |
orderRecord.Type= 'Replacement'; | |
orderRecord.EffectiveDate = Date.today(); | |
orderRecord.OpportunityId = '0065w000023hOsYAAU'; | |
orderRecord.Pricebook2Id = standardPriceBookId; | |
insert orderRecord; | |
List<PricebookEntry> pbeLst = [ | |
Select Id | |
from PricebookEntry | |
Where Pricebook2Id = :standardPriceBookId | |
and Product2Id =:productId | |
limit 1 | |
]; | |
if( pbeLst.isEmpty() ){ | |
return 'Error: Invalid Product. Contact Admin!'; | |
} | |
OrderItem orderItem = new OrderItem(); | |
orderItem.OrderId = orderRecord.Id; | |
orderItem.Quantity = 1; | |
orderItem.UnitPrice = 1500; | |
orderItem.PricebookEntryId = pbeLst[0].Id; | |
orderItem.Product2Id = productId; | |
insert orderItem; | |
List<Order> orderList = [Select OrderNumber from Order Where Id= :orderRecord.Id limit 1]; | |
// Create an approval request for the account | |
Approval.ProcessSubmitRequest approvalReq = | |
new Approval.ProcessSubmitRequest(); | |
approvalReq.setComments('Submitting order for approval.'); | |
approvalReq.setObjectId(orderRecord.Id); | |
Approval.ProcessResult result = Approval.process(approvalReq); | |
if( !orderList.isEmpty() ) { | |
return orderList[0].OrderNumber; | |
} | |
return 'Error: Order is not placed, Contact Admin!'; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class SendWhatsappNotification { | |
@future(callout=true) | |
public static void sendMessage(String fromNumber, String toNumber, String message){ | |
Twillio_Config__c config = Twillio_Config__c.getInstance(); | |
HttpRequest req = new HttpRequest(); | |
req.setEndpoint(config.Endpoint__c); | |
req.setMethod('POST'); | |
String username = config.Account_SID__c; | |
String password = config.Auth_Token__c; | |
Blob headerValue = Blob.valueOf(username + ':' + password); | |
String authorizationHeader = 'Basic ' + | |
EncodingUtil.base64Encode(headerValue); | |
req.setHeader('Authorization', authorizationHeader); | |
req.setHeader('Content-Type', 'application/x-www-form-urlencoded'); | |
if( !toNumber.startsWith('+')) | |
toNumber = '+'+toNumber; | |
if( !fromNumber.startsWith('+')) | |
fromNumber = '+'+fromNumber; | |
toNumber = EncodingUtil.urlEncode('whatsapp:'+toNumber, 'UTF-8'); | |
fromNumber = EncodingUtil.urlEncode('whatsapp:'+fromNumber, 'UTF-8'); | |
message = EncodingUtil.urlEncode(message, 'UTF-8'); | |
String requestBody = String.format( | |
'To={0}&From={1}&Body={2}' | |
, new List<String>{toNumber, fromNumber, message } | |
); | |
req.setBody(requestBody); | |
// Create a new http object to send the request object | |
// A response object is generated as a result of the request | |
Http http = new Http(); | |
HTTPResponse res = http.send(req); | |
System.debug(res.getBody()); | |
//@TODO Error Handling | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class Utils { | |
public static void debugMap(Map<String,Object> mapObj){ | |
debugMap('',mapObj); | |
} | |
public static void debugMap(String prefix,Map<String,Object> mapObj){ | |
for(String key : mapObj.keySet() ) { | |
System.debug(prefix+ ' : '+ key + ' => ' + String.valueOf( mapObj.get(key) ) ); | |
} | |
} | |
public static String sanitize(String msg){ | |
return String.isBlank(msg) ? '' : msg.trim().toLowerCase(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment