Oh my god. It's full of code!

Angel.com

Angel IVR REST API wrapper for Salesforce Apex

Hey all,

Just a random post to help out any developers who may be trying to use the Angel IVR outbound calling features of their new REST API. This is a wrapper class that should do all the hard work for you. It handles all the HTTP traffic, batching, parsing of responses and serialization for ya. You’ll need to create a custom setting called Angel IVR Site and store your API token, API endpoint, subscriber Id in there (or just change the references to settings.whatever in the code to be hard coded. The test class shows creation of one of these objects, along with the expected fields names.

Here is the code. I’ll probably post up a sample app later and maybe even an installable package. I just wanted to get this out there before I forget, or get too lazy to do anything else with it.

/*Angel IVR API Wrapper
Description: Simple class for placing calls via the Angel IVR REST API. Also has experemental
implementations of the other API calls, including cancel, and request (gets job status)

See
https://www.socialtext.net/ivrwiki/outbound_rest_api_documentation
for API details.

Author: Daniel Llewellyn (Twitter: @Kenji776)
Date: 11/16/2012
*/

global class angelIVRWrapper
{
    class applicationException extends Exception {}

    public Angel_IVR_Site__c settings = Angel_IVR_Site__c.getValues('Prod');
    global static boolean isTest = false;

    //a single call item to place to angel. You will always pass a list of these. To make calls create a list
    //of these things (one for each person you wish to call if the call has variables unique to each person, or a single callitem with all the phone numbers included if
    //they are not unique). Then pass them into the campaignCall function along with the angel site you wish to use.
    global class callItem
    {
        public integer maxWaitTime = 30;
        public string phoneNumbers = '';
        public map<string,string> variables = new map<string,string>();
    }

    //a generic API response container. Will contain any error messages and status codes if something bombs.
    //otherwise it should contain a jobId you can later use for cancelling, checking status, etc. Also contains
    //a list of all the call items we attempted to place calls from, all the call items that where skipped (due to being invalid for some resaon)
    //and a list of the responses as provided by angel.
    global class callResponse
    {
        public string jobId = '';
        public string message = 'Calls Placed Successfully';
        public integer httpResponseCode = 200;
        public string httpResponse = 'ok';
        public boolean success = true;
        public list<callRequest> callRequestResponses = new list<callRequest>();
        public list<callItem> placedRequests = new list<callItem>();
        public list<callItem> skippedRequests = new list<callItem>();
    }

    //return object type from the API that contains details about a single call placed using the outbound API.
    global class callRequest
    {
        public string code = '';
        public string callStartTime = '';
        public string callEndTime = '';
        public string phonenumber='';
        public string phoneLineRequestID='';
        public string message = '';
    }    

    @RemoteAction
    global static list<callResponse> campaignCall(list<callitem> callItems, string angelSite)
    {
        angelIVRWrapper controller = new angelIVRWrapper();
        return controller.campaignCall(callItems , angelSite, true);
    }

    //wrapper for the campaignCall function of the Angel IVR. Pass it a list of call items, one for each person you wish to call.
    //it will return a call response object which should contain the job Id you can use for getting the status later.
    public list<callResponse> campaignCall(list<callitem> callItems, string angelSite, boolean allowPartial)
    {
        list<FeedItem> posts = new list<FeedItem>();
        set<string> phoneNumbers = new set<string>();

        //experemental idea for breaking a large number of call items into batches, delayed
        //by a number of seconds. This will likely have to use a recursive scheduled job or something.
        integer batchDelaySeconds = 600;

        list<callResponse> res = new list<callResponse>();
        map<string,string> params = new map<string,string>();
        params.put('allowPartial',string.valueOf(allowPartial));     

        //ask the broker to make a call to angel using the campaignCalls method, passing in the list of call items, and set the allowPartial 
        //url param as well.
        integer counter = 0;
        list<callItem> thisBatch = new list<callItem>();
        list<callItem> skippedRequests = new list<callItem>();

        //break the overall list into batches of 250, since that is the maximum amount you can place in one request.
        for(callItem callItem : callItems)
        {  
            counter++;
            if(callItem.variables.size() <= 50 && !phoneNumbers.contains(callItem.phoneNumbers))
            {              
                thisBatch.add(callItem); 
                phoneNumbers.add(callItem.phoneNumbers);

                if(callitem.variables.containsKey('ID'))
                {
                    FeedItem post = new FeedItem();
                    post.ParentId = callitem.variables.get('ID'); 
                    post.Body = 'Placed outbound call to this record using phone number '+callItem.phoneNumbers+' to Angel site ' + angelSite;    
                    posts.add(post);        
                }                       
            }
            //if this call item has too many variables, or we already have a call for this phone number in the queue, add the call item to our list
            //of skipped calls.
            else
            {
                skippedRequests.add(callItem);
            }

            if(thisBatch.size() == 250 || counter == callItems.size())
            {
                callResponse thisResponse = brokerRequest('POST','campaignCalls',thisBatch, angelSite, params);
                thisResponse.skippedRequests.addAll(skippedRequests);
                res.add(thisResponse);  
                thisBatch.clear();  
                skippedRequests.clear();    
            }
        }
        insert posts;
        return res;
    }

    //wrapper for the requests function of the Angel IVR. Pass it a job id and get back the current status of that job.
    public callResponse requests(string jobId)
    {        
        //ask the broker to make a call to angel using the campaignCalls method, passing in the list of call items, and set the allowPartial 
        //url param as well.  
        callResponse res = brokerRequest('GET','requests/job/'+jobId,null,null,null);        
        return res;
    } 

    //wrapper for the cancels function of the Angel IVR. Pass it a job id and that job will be cancelld if it is currently queued.
    public callResponse cancels(string jobId)
    {
        //ask the broker to make a call to angel using the campaignCalls method, passing in the list of call items, and set the allowPartial 
        //url param as well.  
        callResponse res = brokerRequest('GET','cancels/job/'+jobId,null,null,null);        
        return res;    
    }

    //handles the actual sending of http requests, handling of the response, formatting, etc.
    public callResponse brokerRequest(string httpVerb, string method, list<callitem> callitems, string angelSite, map<string,string> urlParams)
    {
        //create a call response object to pass back.
        callResponse callResponse = new callResponse();

        string requestURI;
        //create the endpoint URI with a bit of string concatination.
        if(angelSite !=null)
        {
            requestURI = settings.API_Endpoint__c+'/'+settings.Subscriber_ID__c+'/'+angelSite+'/'+method+'?apiKey='+settings.API_Key__c;
        }
        else
        {
            requestURI = settings.API_Endpoint__c+'/'+settings.Subscriber_ID__c+'/'+method+'?apiKey='+settings.API_Key__c;
        }    
        HttpRequest req = new HttpRequest();
        //setup the http request.
        req.setMethod(httpVerb);  
        req.setHeader('Content-Type','application/xml');     

        if(urlParams !=null)
        {
            for(string param : urlParams.keySet())
            {
                requestURI += '&'+param+'='+urlParams.get(param);
            }
        }
        req.setEndpoint(requestURI);

        if(callItems != null)
        {    
            //generating Angel XML using the serializer and set that as the request body.                  
            req.setBody(serializeCallItemAngelXml(callitems));
        }
        //send http request
        Http http = new Http();
        HttpResponse res = new HttpResponse();
        string responseBody;
        try
        {
            //some handling for if this is a test class or not. Can't make outbound calls in tests, so we need a mock response
            //if its a test.
            if(!isTest)
            {
                res = http.send(req); 
                responseBody = res.getBody();
            }
            else
            {
                responseBody = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><outboundRequest jobID="0a14021a-1c-13b0b0b3e8a-7abc2f20-d09"><callRequest><requestDetails number="9522206974" phoneLineRequestID="200158136763"/><attempt callEndTime="" callStartTime="" code="queued"><message>queued</message></attempt></callRequest><timeCreated>2012-11-16T16:06:24.331-05:00</timeCreated></outboundRequest>';           
            }

            //if the http response doesn't have a 200 response code, an error happened, so we gotta toss an error, set the success to false, etc.
            //trying to parse whatever is in the body of the http request would probably cause an error as well, so we want to avoid doing that.

            if(res.getStatusCode() != 200 && !isTest)
            {
                throw new applicationException('Error during HTTP Request. Status Code:' + res.getStatusCode() + ' Status:' + res.getStatus() + ' Body Text: ' + res.getBody());
            }

            //if all has gone well until now, parse the results of the http request into a callResponse object and pass that back. This will
            //contain the job id and status of the call(s)
            callResponse = deserializeAngelOutboundRequestXML(responseBody);          
        }
        catch(exception e)
        {          
            callResponse.success = false;
            callResponse.message = e.getMessage() + ' on line: ' + e.getLineNumber() + '. Root cause: ' + e.getCause();   
            callResponse.httpResponseCode = res.getStatusCode();   
            callResponse.httpResponse = res.getStatus();

        }   
        callResponse.placedRequests.addAll(callItems);             
        return callResponse;           
    }

    //takes a list of callItem objects and turns them into valid AngelXML to send to the API. I wish
    //this was more dynamic (using some kind of reflection to iterate over the object properties, but whatever).
    //remember you can include variables in the callItem object to customize the information sent to Angel for each 
    //particular call.
    public static string serializeCallItemAngelXml(list<callitem> callitems)
    {
        string angelXML = '<callItems>';

        for(callitem thisCallItem : callItems)
        {
            angelXML += '<callItem>';
            angelXML += '<maxWaitTime>' + thisCallItem.maxWaitTime + '</maxWaitTime>';
            angelXML += '<phoneNumbers>' + thisCallItem.phoneNumbers + '</phoneNumbers>';
            for(string thisVar : thisCallItem.variables.keySet())
            {
                angelXML += '<variables><name>'+thisVar+'</name>';
                angelXML += '<value>'+thisCallItem.variables.get(thisVar)+'</value></variables>';
            }
            angelXML += '</callItem>';
        }

        angelXML += '</callItems>';
        return angelXML;
    }

    public static callResponse deserializeAngelOutboundRequestXML(string angelXMLResponse)
    {
        Xmlstreamreader reader = new Xmlstreamreader(angelXMLResponse);

        callResponse thisResponse = new callResponse();
        callRequest thisRequest;

        while (reader.hasNext()) 
        { 
            if(reader.getEventType() == XmlTag.START_ELEMENT && reader.getLocalName() == 'outboundRequest')
            {
                thisResponse.jobId = reader.getAttributeValue(null,'jobID'); 
            }  
            if(reader.getEventType() == XmlTag.START_ELEMENT && reader.getLocalName() == 'callRequest')
            {
                thisRequest = new callRequest();
            }            
            else if(reader.getEventType() == XmlTag.START_ELEMENT && reader.getLocalName() == 'requestDetails')
            {
                thisRequest.phonenumber = reader.getAttributeValue(null,'number');
                thisRequest.phoneLineRequestID = reader.getAttributeValue(null,'phoneLineRequestID');
            }
            else if(reader.getEventType() == XmlTag.START_ELEMENT && reader.getLocalName() == 'attempt')
            {
                thisRequest.callStarttime = reader.getAttributeValue(null,'callStartTime');
                thisRequest.callEndtime = reader.getAttributeValue(null,'callEndTime');
                thisRequest.code = reader.getAttributeValue(null,'code');
            }

            else if(reader.getEventType() == XmlTag.START_ELEMENT && reader.getLocalName() == 'message')
            {
                reader.next();
                thisRequest.message = getDecodedString(reader);
            }
            else if(reader.getEventType() == XmlTag.END_ELEMENT && reader.getLocalName() == 'attempt')
            {
                thisResponse.callRequestResponses.add(thisRequest);
            }            
            reader.next();
        }

        return thisResponse;
    }

    public static String getDecodedString(Xmlstreamreader reader)
    {
        return EncodingUtil.urlDecode(reader.getText(), 'UTF-8').trim();
    }

    @isTest
    public static void angelIVRWrapper()
    {
        isTest = true;

        //weird workaround to avoid mixed dml error, as seen here
        //http://boards.developerforce.com/t5/Apex-Code-Development/DML-not-allowed-on-user-in-test-context/m-p/98393
        User thisUser = [ select Id from User where Id = :UserInfo.getUserId() ];
            System.runAs ( thisUser ) {
            Angel_IVR_Site__c settings = new Angel_IVR_Site__c();
            settings.name = 'Prod';
            settings.Angel_Site_Id__c = 'test';
            settings.API_Endpoint__c = 'http://www.test.com';
            settings.API_Key__c = 'test api key';
            settings.Subscriber_ID__c = '402342';
            insert settings;
        }

        angelIVRWrapper controller = new angelIVRWrapper();
        list<angelIVRWrapper.callitem> callItems = new list<angelIVRWrapper.callitem>();
        Respondent__c testRespondent = testDataGenerator.createTestRespondent();

        angelIVRWrapper.callitem thisCallItem = new angelIVRWrapper.callItem();
        thisCallItem.phoneNumbers = '5555555555';
        thisCallItem.variables.put('RESPONDENT__R_NAME','Frank Jones');
        thisCallItem.variables.put('ID',testRespondent.id);

        callItems.add(thisCallItem);

        callResponse requestStatus = controller.requests(response[0].jobId);

        callResponse sendCancel = controller.cancels(response[0].jobId);

        list<callResponse> sendCalls = angelIVRWrapper.campaignCall(callItems, '200000124604');
    }
}

I Hate The Angel.com IVR API

This is a venting post. Very little useful information will be contained within. I guess perhaps it can act as a warning, but honestly, I’m just mad. It’s mostly meant to be cathartic, and perhaps a bit entertaining.

Here is the deal, Angel.com is an IVR provider. They let you make phone calls and set up call sites, etc. They offer an outbound API, which allows you of course, to make calls programmatically. Sounds good in theory. The problem is, I don’t think they really know how to build an API. Here is a quick outline of how I arrived at this conclusion.

1) Limited amount of interactions per API Token
For some reason Angel.com has decided it is wise to only allow a certain amount of API interactions per token. You use your credentials to get a token that is supposed to be valid for a given time period, however it is also only available for like 10 transactions before you need to get a new one. This adds completely unnecessary complexity to working with them. Pretty much every other vendor I have worked with gives you a token that is valid for a time period so you know when you need to get a new one. In this case I have to keep track of how many things I’ve done lest I end up over my limit and have my next call fail. This is lame.

2) Two types of call identifiers
This may be more personal preference than a legitimate gripe, but it does make things more sucky. Of course in a phone system calls can be inbound or outbound. Angel.com has two unique Id’s depending which kind of call it was. Of course this gets complicated because say you use the API to place a call, they give you back an outbound GUID (globally unique identifier), fine. Then someone answers that call and they enter your voice site, now it has an inbound GUID as well. Which one do you use for further transactions? Well apparently the outbound but that was just found out via trial and error as it isn’t documented anywhere.

3) Absolutely terrible getstatus call.
Once a call is placed I need to go back and figure out what happened with it, if it was answered, how long it took etc, so the call time can be billed back to the client. Of course I’ve had to build this complex logging system because Angel doesn’t really have any mechanisms to track which calls pertain to which client. So I have to use the getStatus API call and an outbound GUID (or a list of them), to get the data I need to build my log. This function, is completely, hands down, god awful, for a few reasons.

– Archaic return type: Instead of just having the useful information as text, it’s all returned in these crazy function wrappers. You have to spend half an hour inspecting the data that is returned and playing guess and check to see how to get the data you want. Why, why do I have to do this? What is wrong with returning simple text?

– Very fragile: If even one GUID in the list you send them is invalid for some reason, the whole list aborts. It’s completely all or nothing, meaning you have to get it perfect every time, which wouldn’t be so bad except….

– Random reporting of invalid GUID: For some reason, beyond any comprehension often times I get this error. Angel claims that one or more of the GUIDs I give them do not belong to me. Then where the fuck did I get them smart guy? You think I just conjured some fucking GUID’s out of thin air? Pulled them out of my ass and thought it would be funny for you to try and parse them? No you slack jawed backwards ass API, you GAVE THEM TO ME. I placed the call using the API, recorded the GUID in my database and now I’m trying to figure out what happened. Don’t you tell me it’s invalid, you gave it to me. Fine though, if you say it’s bad I’ll remove it from my list and pretend it doesn’t exist, well I’d love to except…

– Horrible error data reporting: If you get the above error, good luck trying to figure out what GUID is causing the problem. They give you the list of them, sure… but it’s just in a string.. after some error text, part of a stack trace and wrapped in some HTML formatting. That’s right… HTML formatting, in a web service call error message return. Not only do they not return an array with each GUID and it’s status, no, you get one text blob with the GUID’s that are presumable the problem just kind of… hanging out in there. Just chilling among the human readable error, the stack track and some mother fucking HTML.

annot perform web service invocation getStatus.The fault returned when invoking the web service operation is:<br> <pre>AxisFault faultCode: {http://schemas.xmlsoap.org/soap/envelope/}Server faultSubcode: faultString: Some of the CallGUIDs that you have submitted do not belong to you. SubscriberID: 40225 token: 0a14022d-0b-132634f962b-685b305c-ccb@40225@403e4852@1315926152748@1@1cef1c329a519114c4922915bbca946b GUID: [list of GUIDs here]

What the fuck is this shit?

GUID’s don’t want to be in that mosh pit, they belong as their own data structure that I can iterate over easily so I can mark that I shouldn’t try to get them again. Even after parse the text blob, cleaning the HTML, extracting the GUIDs and attempting to update my database so that I don’t attempt to get those statuses again, then I get this error…

– stackTrace:The email entered does not match our records. – WHAT. THE. FUCK. What the hell is this error? Email does not match your records? My credentials are hard coded in. These credentials just worked moments ago. Why now, all of a sudden as if being randomly smote by Thor does this error crop up? No real explanation, no reasoning, just error. Your credentials are bad, but yet good enough to get this far? My credentials half work? They exist in some kind of quantum super position of working and not working at the same time. Suck my ass.

So that’s about it. The main reasons I hate Angel. Apparently they have some new REST API which I’m sure they’ll tout as being better, but that doesn’t do me a ton of good seeing as I’ll have to rewrite my entire integration library to use it, and I have no guarantee it doesn’t suck just as hard.

Whew, I feel a little better now.


Coldfusion, Angel.com, Google Maps Directions, and You!

Okay, so even the title is a mouthful, this post is probably going to be insane you are thinking. Well… maybe, but it’s cool stuff. So picture this. You are using Angel.com as an IVR provider. So people call in and talk to a phone machine for data. Now say you want to give directions over the phone. Say you want those directions to be dynamic, and given to the user step by step. So for example you are hosting an event. People pre-register for this event, and have provided their address, which you have stored in a database. Bob Johnson calls in (he has registered and provided his address before) and wants directions from his house to your event center. You might think you’d need a live person to do this. Blasphemy! Have Bob authenticate so we can find his address in the database. Feed that address into an Angel.com variable (if there were a better way to enter addresses over the phone, you wouldn’t even need to pre-register, but because entering data in phones sucks we kind of need their address to already exist somewhere we can get it). Once that variable is in Angel.com, pass it, along with the destination to this tool via URL arguments. This tool will then give step by step directions that the IVR can read aloud back to Bob. He even has the ability to replay each direction, and navigate backward and forward through the steps.

Just copy and paste this and run host it on a ColdFusion server somewhere. It’s ready to be called with all configs just being passed in the URL at runtime.

<cfparam name="url.start" type="string" default="1405+Olive+Ln+N,+Plymouth,+Hennepin,+Minnesota+55447">
<cfparam name="url.end" type="string" default="1111+Cambridge+St.+Hopkins,+MN+55343+(White+Castle)">
<cfparam name="url.stepID" type="integer" default="1">

<!---- The id of the page that calls this webservice in angel.com ---->
<cfparam name="url.thisPage" type="string" default="1">

<!---- the id of the page to go to if this thing errors for some reason --->
<cfparam name="url.failPage" type="string" default="2">

<!---- the id of the page to go when we are all done giving directions ---->
<cfparam name="url.nextPage" type="string" default="3">

<!---- the id of the page to go to if we just can't find directions or a route ---->
<cfparam name="url.reEnterInfoPage" type="string" default="4">

<!---- the id of the page to go to if the person decides they want to talk to a person ---->
<cfparam name="url.transferToCC" type="string" default="5">

<!---- the id of the page to go to if the person wants to hang up---->
<cfparam name="url.disconnectPage" type="string" default="6">

<cfparam name="XMLData" type="string" default="">
<cfparam name="text" type="string" default="">

<!--- Format the directions for sending to google --->
<cfset url.start = replacenocase(url.start, " ", "+")>
<cfset url.end = replacenocase(url.end, " ", "+")>

<cfhttp url="http://maps.google.com/" result="KMLData">
    <cfhttpparam name="saddr" value="#url.start#" type="url">
    <cfhttpparam name="daddr" value="#url.end#" type="url">
    <cfhttpparam name="output" value="kml" type="url">
</cfhttp> 


<cfoutput>
    <cftry>
        <cfset XMLData = xmlParse(KMLData.FileContent)>
        <cfset totalNumberOfSteps = arraylen(XMLData.kml.Document.XmlChildren)-4>
        <cfset text = XMLData.kml.Document.XmlChildren[stepID+3].XmlChildren[1].XmlText>
        <cfset text = text&" . .">
        <!--- Some extra text formatting for reading over the IVR. You can easily add more abbreviations here if there
              are some I forgot --->
        <cfset text = replacenocase(text, " LN ", " Lane ")>
        <cfset text = replacenocase(text, " BLVD ", " Boulevard ")>
        <cfset text = replacenocase(text, " RD ", " Road ")>
        <cfset text = replacenocase(text, " ST ", " Street ")>
        <cfset text = replacenocase(text, " Ave ", " Avenue ")>
        <cfset text = replacenocase(text, " NW ", " North West ")>
        <cfset text = replacenocase(text, " NE ", " North East ")>
        <cfset text = replacenocase(text, " SE ", " South East ")>
        <cfset text = replacenocase(text, " SW ",  "South West")>            
        <cfset text = replacenocase(text, " N ", " North ")>
        <cfset text = replacenocase(text, " E ", " East ")>
        <cfset text = replacenocase(text, " W ", " West ")>
        <cfset text = replacenocase(text, " S ",  "South ")>

        
        <cfsavecontent variable="PromptMessage">
            #text#
            <cfif stepID LT totalNumberOfSteps>
                Press 1 for the next direction. Press 2 to repeat this direction. Press 3 to hear the previous direction.
                <cfelse>
                    You have reached your destination.
                    Press 1 to disconnect. Press 2 to repeat this direction. Press 3 to hear the previous direction.
            </cfif>    
        </cfsavecontent>
    
        <cfif stepID LT totalNumberOfSteps>
            <cfsavecontent variable="XMLLinks">
                <LINK dtmf="1" returnValue="#stepID+1#" destination="#url.thisPage#" />
                <LINK dtmf="2" returnValue="#stepID#" destination="#url.thisPage#" />
                <LINK dtmf="3" returnValue="#stepID-1#" destination="#url.thisPage#" />
            </cfsavecontent>
            
            <cfelse>
                <cfsavecontent variable="XMLLinks">
                    <LINK dtmf="1" returnValue="#url.nextPage#" destination="#url.thisPage#" />
                    <LINK dtmf="2" returnValue="#stepID#" destination="#url.thisPage#" />
                    <LINK dtmf="3" returnValue="#stepID-1#" destination="#url.thisPage#" />
                </cfsavecontent>            
        </cfif>
        
        
        <cfset counter = 1>
        <cfset VariablesObject[counter] = structnew()>
        <cfset VariablesObject[counter]["Name"] = "totalDirections">
        <cfset VariablesObject[counter]["Value"] = totalNumberOfSteps>

         

        <cfcatch type="any">
            <cfset promptMessage = "Sorry we couldn't find a route with the information supplied. Press 1 to try a different direction method. Press 2 to disconnect, or press 3 to be transferred to customer care.">
            <cfsavecontent variable="XMLLinks">
                <LINK dtmf="1" returnValue="#reEnterInfoPage#" destination="#failPage#" />
                <LINK dtmf="2" returnValue="#disconnectPage#" destination="#url.thisPage#" />
                <LINK dtmf="3" returnValue="#transferToCC#" destination="#url.thisPage#" />
            </cfsavecontent>        
            

            <cfset VariablesObject[1] = structnew()>
            <cfset VariablesObject[1]["Name"] = "ErrorType">
            <cfset VariablesObject[1]["Value"] = cfcatch.Type>        

            <cfset VariablesObject[2] = structnew()>
            <cfset VariablesObject[2]["Name"] = "ErrorMessage">
            <cfset VariablesObject[2]["Value"] = cfcatch.Message>

            <cfset VariablesObject[3] = structnew()>
            <cfset VariablesObject[3]["Name"] = "ErrorDetails">
            <cfset VariablesObject[3]["Value"] = cfcatch.Detail>                                    
        </cfcatch>
    </cftry>    
    
    <cfset ReturnObject = printQuestionReturnVariables('stepID',PromptMessage,XMLLinks,url.failPage,VariablesObject)>
#trim(ReturnObject)#
</cfoutput>


<cffunction name="printQuestionReturnVariables" access="remote" hint="Print Question Data For Angel IVR with returnable variables">
    <cfargument name="varName" default="none" type="string">
    <cfargument name="promptMessage" default="none" type="string">
    <cfargument name="linkMessage" default="none" type="string">
    <cfargument name="failPage" default="failPagePlaceholder" type="string">
    
    <!--- This is an array of structures, with keys "name" and "value" --->
    <!--- EX variables[1].Name = Gender --->
    <!--- EX variables[1].Value = Male --->
    <cfargument name="variablesToInclude" default="" type="any" required="no">
    
    <!--- create, scope and set the loop counter variable used below --->
    <cfset var i = 0>
    
    <cfoutput>
        <cfsavecontent variable="ReturnMessage">
            <ANGELXML>
                <QUESTION var="#ucase(varname)#">
                    <PLAY>
                        <PROMPT type="text">
                            #promptMessage#
                        </PROMPT>
                    </PLAY>
                    <RESPONSE>
                        <KEYWORD>
                            #linkMessage#
                        </KEYWORD>
                    </RESPONSE>
    
                    <ERROR_STRATEGY type="nomatch" reprompt="true">
                        <PROMPT type="text"> Sorry I did not get that. </PROMPT>
                        <PROMPT type="text"> I still did not get that. </PROMPT>
                        <PROMPT type="text"> Since I am having so much trouble; please hold while I transfer you to a customer representative who can better serve you. </PROMPT>
                        <GOTO destination="/25" />
                    </ERROR_STRATEGY>
                    <ERROR_STRATEGY type="noinput" reprompt="true">
                        <PROMPT type="text"> Sorry I did not get that. </PROMPT>
                        <PROMPT type="text"> I still did not get that. </PROMPT>
                        <PROMPT type="text"> Since I am having so much trouble; please hold while I transfer you to a customer representative who can better serve you. </PROMPT>
                        <GOTO destination="/25" />
                    </ERROR_STRATEGY>
                </QUESTION>        
                <cfif IsArray(arguments.variablesToInclude)>
                    <VARIABLES>
                        <cfloop from="1" to="#arraylen(arguments.variablesToInclude)#" index="i">
                            <cftry>
                                <cfif structkeyexists(arguments.variablesToInclude[i],'Name') and  structkeyexists(arguments.variablesToInclude[i],'value')>
                                    <VAR name="#ucase(arguments.variablesToInclude[i]['Name'])#" value="#arguments.variablesToInclude[i]['Value']#" />
                                </cfif>
                                <cfcatch type="any">
                                    <cfset ErrorData = structnew()>
                                    <cfset ErrorData.Error = cfcatch>
                                    <cfset ErrorData.Arguments = arguments>
                                    <cfset ErrorData.form = form>
                                    
                                    <!--- You might wanna email this data to yourself or something --->
                                 
                                </cfcatch>    
                            </cftry>                            
                        </cfloop>

                    </VARIABLES>
                </cfif>    
                
            </ANGELXML>
        </cfsavecontent>
    </cfoutput>
    
    <cfreturn ReturnMessage>
</cffunction>

So really what’s happening here is that the page gets called with some URL variables. Those variables are used to construct a google maps http request. That request actually prints out XML. We take the XML and clean it up a little bit and format it. Then print it off in a nice Angel XML package so it can be read by the system. The page just provides one step at a time, and uses a recursive style setup to just continually give directions until there are no more.

If you just want to use the step by step direction giving, you can of course easily remove all the Angel XML junk and just access the #return# variable and do whatever you like with it. This could easily be adapted to an online direction giver, or for Twilio, SMS, whatever. For now it is ColdFusion based, but I may try and convert it to an Apex class in the not too distant future. Depending on how the user is interacting with this, you could even remove the need to have the address pre stored. You could for example have a dedicated number where users just text their current address and you text them back step by step directions to your office or whatever. Really the sky is the limit here.

Here is a sample to see how it works. The demo has some extra stuff to make it more “person usable” instead of stripped down to be consumed by a computer.
Example of ColdFusion/Google Maps/Angel.com dynamic directions

Anyway, hope you guys think this is cool. It was a lot of fun to write!


Salesforce and Angel.com IVR

Salesforce is one of the largest (if not the largest) CRM provider. Angel.com is one of the largest interactive voice response unit providers. It’s likely at some point you are going to want to get a phone system to work with Salesforce, and thankfully it is fairly easy to do. Say for example you wanted to make a simple application that let a person check their Salesforce case status from the phone. It might work something like this…

1) Person calls in
2) IVR prompts person for their case number
3) IVR sends case number to Salesforce
4) Salesforce responds to the IVR with the case status and responsible agent.
5) IVR reads back the information to the person
6) End the call

(I know you’d want to add more features, but we’ll keep it simple for now). The part you are probably wondering about is how steps 3 and 4 work. We’ll lucky you I’m going to take you step by step through on how to do this.

Step 1: Build the Angel.com IVR site
First, your going to need a site which people can call into. Log in and create a new voice site. You’ll want to create 4 pages. First page should be some kind of welcome message that tells the person the service this site will offer. 2nd page will be a question page where they are prompted to enter their case number. 3rd page will be a transaction page that sends the case number to Salesforce, gets a response back and reads the results. The 4th page will be a message page that thanks them and disconnects.
Case Managment site in Angel

A the 2nd and 3rd page are the really important ones The 2nd page asks the person for their case number and stores it in variable. You can call that variable whatever you like, I just called it caseNumber. Salesforce case numbers are 8 digits, so made sure that a person has to enter 8 digits before moving on. Like so..

Get Case Info Page

The setup for page 20

The 3rd page is the transaction page responsible for sending the information to Salesforce. It will send the caseNumber with either the Get or Post method (use Get). I always include at least 2 other variables when I make transaction page calls, that I call nextPage and failPage. NextPage is where the XML should point the IVR to go if everything worked as expected. FailPage is where the IVR should go it the code encounters some kind of problem or unexpected result.This would normally bounce the person to customer support or something. For this example I am also including a retryPage, so if a case is not found the person can enter another case number if they want. Anyway, after that we tell it to expect the results as Angel XML (Angel’s XML data format, go figure).

Transaction Page

The page that sends info from Angel to SF

That is all we have to do on the Angel side for now, lets get the Salesforce part set up. This will consist of few steps.
1) Build the Salesforce site to house the required page.
2) Create the Class that actually gets and formats the data
3) Create the visual force page that will be called by the IVR and gets info from the above class
4) Write testing class and deploy.

So go ahead and log into your Salesforce org. Create a new Salesforce site. I called it IVR. This is my site for all kinds of IVR pages and classes. My setup looks like this.

Salesforce IVR Site

The setup for the Salesforce IVR site. Pretty simple.

Next we need to create a class that will take a case number, get the status and description and return it in some Angel XML. So fire eclipse (or the web based IDE if that floats your boat) and create a new class. I’m terrible at organizing code (never really been on a real dev team so never had anyone teach me) so I’m just going to call it getCaseInfoForAngel.

public class getCaseInfoForAngel 
//The default here is "public class with sharing" but that doesn't work. Remove the "with sharing"
{
    //Create a public variable to store the XML we will return to Angel. 
    public string AngelXML {get;set;}
    
    //Create the method that gets the info
    public void getInfoByCaseNum() 
    {
        //Take any variables passed in GET or POST and make them available in the params structure
        Map<string,string> params = ApexPages.currentPage().getParameters();
        
        //Variable to hold the text that will be read by the IVR to the user
        String promptMessage;
        
        //Variable that holds the mappings of the buttons a user can press to where that button will take them (ex press 1 to re-enter info)
        String xmlLinks = '';
        
        //a list of "structures" (a custom data type that contains name and value pairs) that can returned to Angel.
        //We won't need to pass any variables back to Angel for this example, but we'll just keep there here for future reference.
        List<Struct> angelContacts = new List<Struct>();

        List<Case> records = new List<Case>();
        
        try
        {        
            records = [Select Id, Status, Description
                                  from case
                                  where caseNumber = :params.get('caseNumber')];
               
               //If we found a case number
               if (!records.isEmpty()) 
            {    
                //Set the prompt message to read
                promptMessage = 'Your case status is '+records[0].Status+'. '+records[0].Description+'.';
                xmlLinks += '<LINK dtmf="1" returnValue="1" destination="'+params.get('nextPage')+'" />';
            }
            else
            {
                promptMessage = 'We were unable to locate any information for a case with that number. Press one to enter your case number again or two to disconnect.';
                xmlLinks += '<LINK dtmf="1" returnValue="1" destination="'+params.get('retryPage')+'" />';
                xmlLinks += '<LINK dtmf="2" returnValue="2" destination="'+params.get('nextPage')+'" />';
            }
                
        }
        catch(Exception e)
        {
                promptMessage = 'Sorry but we have encountered and error while getting your case information. Press 1 to enter your case number again or two to disconnect.';
                xmlLinks += '<LINK dtmf="1" returnValue="1" destination="'+params.get('retryPage')+'" />';
                xmlLinks += '<LINK dtmf="2" returnValue="2" destination="'+params.get('nextPage')+'" />';            
        }    
        
        //Create an Angel XML return object              
        createAngelXML A = new createAngelXML(); 
        
        //Set the AngelXML to the results of the call to the createAngelXML method.
        AngelXML = A.createXML('caseInfo',promptMessage,xmlLinks,params.get('failPage'),angelContacts);
    }
    public String getResult() 
    {
        //Return the results
        return AngelXML;    
    }
}

You’ll notice you are missing two things, the struct class, and the createAngelXML class. These are both supporting classes I wrote. Again, they are piss poor, not very flexible but they do the job and are easy to understand. I’m sure a real programmer can make these suck less, but they work for now.

Struct.cls

global class Struct {

    public String name;
    public String value;

    public Struct (String name, String value)
    {
         this.name = name;
         this.value = value;
    }
}

CreateAngelXML.cls

global class createAngelXML 
{
    public String createXML(String variableName, 
                          String textMessage, 
                          String linkMessage, 
                          String failPage,
                          List<Struct> AngelVariables)
    {

        String extraVariables = '';
        
        if(!AngelVariables.isEmpty())
        {
            extraVariables = '<VARIABLES>';
            for (Struct s: AngelVariables) 
            {
                extraVariables += '<VAR name="'+s.name+'" value="'+s.value+'" />';
            }
            extraVariables += '</VARIABLES>';
        }
        String responseText = '<ANGELXML>'+
                '<QUESTION var="'+variableName+'">'+
                    '<PLAY>'+
                        '<PROMPT type="text">'+
                            textMessage+
                        '</PROMPT>'+
                    '</PLAY>'+
                    '<RESPONSE>'+
                        '<KEYWORD>'+
                            linkMessage+
                        '</KEYWORD>'+
                    '</RESPONSE>'+
                    '<ERROR_STRATEGY type="nomatch" reprompt="true">'+
                        '<PROMPT type="text"> Sorry I did not get that. </PROMPT>'+
                        '<PROMPT type="text"> I still did not get that. </PROMPT>'+
                        '<PROMPT type="text"> Since I am having so much trouble; please hold while I transfer you to a customer representative who can better serve you. </PROMPT>'+
                        '<GOTO destination="'+failPage+'" />'+
                    '</ERROR_STRATEGY>'+
                    '<ERROR_STRATEGY type="noinput" reprompt="true">'+
                        '<PROMPT type="text"> Sorry I did not get that. </PROMPT>'+
                        '<PROMPT type="text"> I still did not get that. </PROMPT>'+
                        '<PROMPT type="text"> Since I am having so much trouble; please hold while I transfer you to a customer representative who can better serve you. </PROMPT>'+
                        '<GOTO destination="'+failPage+'" />'+
                    '</ERROR_STRATEGY>'+
                '</QUESTION>    '+  
                    extraVariables+
            '</ANGELXML>';
            
            return responseText;
    
    }
    
}

So with those, your getCaseInfoForAngel class should have everything it needs. It will take a case number, get the details and print them back in Angel’s expected format. Now we just need the visual force page to actually invoke that class. So create a new visualForce page. Call it whatever you want, I’m calling mine getCaseInfo It’s code will look like this.

<apex:page controller="getCaseInfoForAngel"  action="{!getInfoByCaseNum}"
contentType="application/x-JavaScript; charset=utf-8" showHeader="false" standardStylesheets="false" sidebar="false">
{!result}
</apex:page>

Alright, getting close, the hard stuff is done. Now we need to write the testing code, deploy it, adjust the site security, feed angel the new data about our site, and we see if it runs.

I’m not a big fan of testing classes, for me the best test is just to see if my stupid code runs, so I try go get by on as a little work as possible. For this class that means we need to create a case so we can get a case number. Which means we need a contact. Which means we need an account (You can see why test classes bother me, sigh). Anywho, here is some pre packaged testing code. It includes both a success case, and a failure case. Should get pretty close to 75% coverage I think.

       //create a new date object so we can easily insert it
        DateTime dT = System.now();
        Date myDate = date.newinstance(dT.year(), dT.month(), dT.day());

        //make an account so we can attach a contact to it
        Account TestAccount = new Account(name='My Test Account', Tax_ID__c='99-9999999');    
               
        //Now make a contact, so we can attach him as a respondent a few times
        date birthdate = date.parse('03/21/1988');
        Contact ContactGuy1 = new Contact(Firstname='Frank', 
                                            Lastname='Jones', 
                                            AccountID=TestAccount.ID, 
                                            of_PSA__c=0, 
                                            of_No_shows__c =0, 
                                            of_Cancellations__c =0, 
                                            of_Disqualified__c =0, 
                                            of_Participations__c =0, 
                                            MailingCity='coon rapids', 
                                            MailingCountry = 'us', 
                                            MailingState='MN', 
                                            MailingStreet='11021 bittersweet street', 
                                            MailingPostalCode='55433',
                                            phone = '7632344306',
                                            birthdate = birthdate);

        insert ContactGuy1;

        //Try to create the same case twice. Second one should error.
        Case case1 = new case(contact = ContactGuy1, status = 'Open', Origin = 'Web', subject='This is a test case. Eat face');
        insert case1;

        //***************** TEST getCaseInfo Class ********************************
        
        //Working test
        try
        {
            Case testCase = [select CaseNumber from case where id = :case1.id];
            PageReference getCaseInfo = Page.getCaseInfo;
            getCaseInfo.getParameters().put('caseNumber',testCase.CaseNumber);

            Test.setCurrentPageReference(getCaseInfo);
            getCaseInfoForAngel getInfoByCaseNumCtrl = new getCaseInfoForAngel ();
            getInfoByCaseNumCtrl.getInfoByCaseNum();
            myRunResult = getInfoByCaseNumCtrl.getResult(); 
        }
        catch(Exception e)
        {
            
        }
        
        //failure test    
        try
        {
            PageReference getCaseInfo = Page.getCaseInfo;
            getCaseInfo.getParameters().put('caseNumber','000000');

            Test.setCurrentPageReference(getCaseInfo);
            getCaseInfoForAngel getInfoByCaseNumCtrl = new getCaseInfoForAngel ();
            getInfoByCaseNumCtrl.getInfoByCaseNum();
            myRunResult = getInfoByCaseNumCtrl.getResult(); 
        }
        catch(Exception e)
        {
            
        }

So now go ahead and get your deployment ready. Select struct.cls, CreateAngelXML.cls, getCaseInfoForAngel.cls, your testing class, and your getCaseInfo visual force page and push them into prod (if you want, you can test in your sandbox, doesn’t matter really). Hopefully it should all deploy. If not post in the comments and I’ll try to help if I can.

Now we need to include all that stuff in our IVR Salesforce site. So go to your site configs, click public access settings. Modify the profile to have access to case info, and contact info just to be safe (since cases are related to contacts, not having contact info might cause issues, I dunno). Head to the enabled Apex classes section, and include struct.cls, createAngelXML.cls, and getCaseInfoForAngel.cls. Go to the enabled Apex Pages, and include getCaseInfo. If any of these things are not available in the lists, you might have to ajust their individual security settings. For example, my VisualForce page wasn’t available for me to select at first. So go to develop->pages->getCaseInfo->Security. Then I just enable it for all profiles to make life easy. I’m not sure which profile is that list is the one that does it, probably whatever profile your current user account has. EX

Setting VisualForce Page Permissions

f you are not able to select a needed page or class in Public Access Settings area, you may need to adjust the resources security like so

Okay, so now all classes and pages should be enabled for your site. You should now be able to access the getCaseInfo page. It would be something like
http://YourSalesForceSite.force.com/IVR/getCaseInfo
With any luck visitng that page should print out some AngelXML. If you get a Salesforce security message, you probably forgot a setting somewhere. It also shows that message if your code has an error, but my code never errors :P. The sample code I posted did work for me, so I’m pretty sure it does run. If you try it without giving it any case number it should print out the message “We were unable to locate any information for a case with that number”. You can give it a case number by adding a ?caseNumber=xxxxxxxx at the end to see what it does. It should now print our your case details.

So now with our functioning page, we need to hook it up to Angel. Go back to your transaction page in Angel. For the URL you can now enter the URL of your page. You can plug some test values in the boxes and see what happens. You should see it return some happy XML like so.

Happy XML Results!

Huzzah it worked!

And there you have it, you’ve built your first simple Salesforce and Angel.com integrated app. The beginnings of a customer service platform that lives totally in the cloud. Feel free to post code upgrades, questions, comments, whatever. I hope this helped some people out there, as it took me a long time to figure out, and almost as long to write about 😛