Guest Post: Acumatica + Twilio = A Hackathon Solution in 4 Hours

Mark Franks | September 12, 2017

Written by Marco Villaseñor
Technology Director
Syntegh
Twitter: @mvillasenor | @syntegh
LinkedIn: Syntegh 

 Connected cloud servicesConnected Clouds

With the growth of the web, there has also been a boom of software companies that provide access to their applications through web APIs and even offer microservices for very specific functions that help build complex and more powerful applications.

Why is this interesting? Well, this rise of APIs has created an expanding ecosystem of services and tools businesses can integrate into their own processes. If their systems allow it, a company can focus on its core operation while benefiting from functionality that would take too long to develop or maintain to be cost effective.

One of the most appealing features of Acumatica xRP
platform is how easy it is to connect with other web applications and
integrate them to the core modules.

I like to think Acumatica has cloud in its DNA and it really shows when we work on integrations: it’s much easier to work with applications and services that provide a web API. If we need to work from within an external application, Acumatica allows us to easily expose data via OData and business objects through SOAP and REST web services. On the other hand, if we need to consume a service from within Acumatica, its Customization Platform provides tools to embed the new features into existing parts of the ERP.

A Practical Example

Last February, we participated in the first Acumatica Hackathon held during the Acumatica Summit in San Diego. Our team decided to work on a project that showed the power of “connecting clouds” by adding voice and SMS notifications to Acumatica by using Twilio.

Twilio is a cloud communications platform that provides web APIs to add messaging, voice, and video in web and mobile applications. The module and its source code are available in github: Acumatica-Twilio.

This project serves as a good example of how quickly it is to build a self contained module that adds specific functionality to Acumatica, so let’s do a step-by-step review of how this module was built.

Project Design

The first thing we needed to do is create a new customization project so the new functionality can be deployed and even shared for publication in other instances. The following diagram shows an overview of all the module parts:

 

Using Twilio’s API

In order to send SMS and Voice notifications from Acumatica using Twilio, we needed to consume its web API.
Luckily, there is a Twilio C#/.Net helper library which handles all requests and can be referenced directly.

So we added a reference to the library to our project using NuGet:

Install-Package Twilio

We then built a class to wrap the library and expose two simple methods that allowed us to either send an SMS message or start a call:

private TwilioRestClient _client;
public string Origin { get; set; }

public TwilioNotification(string sid, string token)
{
  _client = new TwilioRestClient(sid, token);
}

public void SendSMS(string number, string message)
{
  var msg = _client.SendMessage(Origin, number, message);

  if (msg.RestException != null)
  {
    throw new PXException(msg.RestException.Message);
  }
}

public void SendCall(string number, string message)
{
  var call = _client.InitiateOutboundCall(Origin, number, MessageUrl(message));

  if (call.RestException != null)
  {
    throw new PXException(call.RestException.Message);
  }
}

If this looks simple, it’s because it is! For the SMS notifications, we just provide an origin and destination number along with the desired text message and Twilio does the rest.

Voice notifications need a little more of preparation. For voice, Twilio offers a simple text-to-speech service that we can use to read messages as audio. This service requires us to provide a callback URL where their engine looks for instructions of what to do when the call is connected.

We handled this by using one of the utility services offered by Twilio called “twimlets”. These micro apps generate Twilio instructions on the fly from the information they get on the URL query string.

By using the simple message twimlet, we can just build an URL with our message as part of the query string and the twimlet will build the required code for Twilio to say it.

private static string TwimletBase = "http://twimlets.com/message";

...

public static string MessageUrl(params string[] messages)
{
  var messageCollection = new NameValueCollection();

  for (int i = 0; i < messages.Length; i++)
  {
    messageCollection.Add("Message[" + i + "]", messages[i]);
  }

  return TwimletBase + messageCollection.ToQueryString();
}

Setup Screen

In order to create an instance of the client library, we needed to provide the credentials of a Twilio account, so we programmed a Setup screen to enable any user to enter their own.

It’s always a good practice to use the PXRSACryptString attribute provided by Acumatica to encrypt the credentials in the new database table.

#region TwilioAccountSid

public abstract class twilioAccountSid : IBqlField { }

[PXRSACryptString(255)]
[PXDefault]
[PXUIField(DisplayName = "Twilio Account Sid")]
public string TwilioAccountSid { get; set; }

#endregion

#region TwilioAuthToken

public abstract class twilioAuthToken : IBqlField { }

[PXRSACryptString(255)]
[PXDefault]
[PXUIField(DisplayName = "Twilio Auth Token")]
public string TwilioAuthToken { get; set; }

#endregion

Twilio requires sending the original phone number for voice notifications, so we also added a field to store this number so it is always sent in the call request.

Finally, we also added the option of selecting the notification templates used for voice and SMS notifications.

#region SMSNotificationID

public abstract class sMSNotificationID : IBqlField { }

[PXDBInt]
[PXDefault]
[PXSelector(typeof(Notification.notificationID),
    DescriptionField = typeof(Notification.name), ValidateValue = true)]
[PXUIField(DisplayName = "Notification ID")]
public int? SMSNotificationID { get; set; }

#endregion

#region CallNotificationID

public abstract class callNotificationID : IBqlField { }

[PXDBInt]
[PXDefault]
[PXSelector(typeof(Notification.notificationID),
    DescriptionField = typeof(Notification.name), ValidateValue = true)]
[PXUIField(DisplayName = "Call Notification ID")]
public int? CallNotificationID { get; set; }

#endregion

Once we defined the Setup DAC, we created a simple Graph for it and the related Page as TW101000 and added it to the Site Map under Management -> Configure -> Twilio Integration.

TW101000 - TwilioSetup

Adding Actions

Now comes the interesting part: adding the action to trigger a notification. The Acumatica Customization Platform allows us to extend existing business logic by using the PXGraphExtension class.

For our project, we decided to send notifications for invoices with due balance, but this behavior can easily be added to other business objects.

First, we created an extension of the ARInvoiceMaint graph and added a private helper function to handle sending the notifications. Depending on its type, the function prepares the content from the NotificationID configured in the setup graph and calls our Twilio class to send the message.

public void SendTwilioNotification(ARInvoiceEntry invGraph, string notificationType, bool isMassProcess = false)
{
    ARInvoiceEntryPXExt invGraphExt = invGraph.GetExtension<ARInvoiceEntryPXExt>();

    //Raise error if Twilio Integration Setup not configured
    if (invGraphExt.TwilioSetupInfo.Current == null)
        throw new PXException(Messages.TwilioAccountNotSetup);

    //Get current billing contact
    if (invGraph.Billing_Contact.Current == null)
        invGraph.Billing_Contact.Current = invGraph.Billing_Contact.Select();
    ARContact contact = invGraph.Billing_Contact.Current;

    if (contact == null || String.IsNullOrEmpty(contact.Phone1))
        throw new PXException(Messages.InvoiceBillingContactNotExists);

    int? iNotificationID = (notificationType == TwilioNotificationType.SMS) ?
                            invGraphExt.TwilioSetupInfo.Current?.SMSNotificationID :
                            invGraphExt.TwilioSetupInfo.Current?.CallNotificationID;

    //Raise notification appropriate error message
    if (notificationType == TwilioNotificationType.SMS)
    {
        if (iNotificationID == null)
            throw new PXException(Messages.SMSNotificationIDNotSpecified);
    }
    else if (notificationType == TwilioNotificationType.OutBoundCall)
    {
        if (iNotificationID == null)
            throw new PXException(Messages.CallNotificationIDNotSpecified);
    }
    else
        throw new PXException(Messages.UNSpecifiedTwilioNotificationType);

    //Get the notification
    PX.SM.Notification notification = PXSelect<PX.SM.Notification,
                            Where<PX.SM.Notification.notificationID, Equal<Required<PX.SM.Notification.notificationID>>>>.
                            Select(invGraph, invGraphExt.TwilioSetupInfo.Current.SMSNotificationID);

    if (notification == null)
        throw new PXException(Messages.NotificationNotFound);

    //Process the datafields and get Subject and Notification Body ready.
    string subjectNotification = String.Format("{0} - {1}", (notificationType == TwilioNotificationType.SMS) ?
                                    Messages.SMSSubjectPrefix : Messages.CallSubjectPrefix,
                                    PX.Data.Wiki.Parser.PXTemplateContentParser.
                                    Instance.Process(notification.Subject, invGraph,
                                                     invGraph.Document.Current.GetType(), null));
    string bodyNotification = PX.Data.Wiki.Parser.PXTemplateContentParser.
                                 Instance.Process(notification.Body, invGraph,
                                                  invGraph.Document.Current.GetType(), null);

    //Create Twilio Notification
    var twilio = new TwilioNotification(invGraphExt.TwilioSetupInfo.Current.TwilioAccountSid,
                                        invGraphExt.TwilioSetupInfo.Current.TwilioAuthToken)
    {
        Origin = invGraphExt.TwilioSetupInfo.Current.TwilioFromPhoneNumber
    };

    if (notificationType == TwilioNotificationType.SMS)
    {
        twilio.SendSMS(contact.Phone1, PX.Data.Search.SearchService.Html2PlainText(bodyNotification));
    }
    else if (notificationType == TwilioNotificationType.OutBoundCall)
    {
        twilio.SendCall(contact.Phone1, PX.Data.Search.SearchService.Html2PlainText(bodyNotification));
    }
    else
        throw new PXException(Messages.UNSpecifiedTwilioNotificationType);

    //Create Activity in Acumatica
    CreateTwilioNotificationActivity(invGraph, subjectNotification, bodyNotification);
}

The last line of the function was added later to add the notification to the document activity as a nice add-on. Please review the full code to see how this was done.

After this, we added the actions to trigger each notification type. This is done by using the [PXProcessButton] attribute to each method. We wrapped the SendTwilioNotification in a PXLongOperation to send the notifications asynchronously and avoid blocking the user interface.

#region Actions

    public PXAction<ARInvoice> SendSMSNotification;

    [PXUIField(DisplayName = "Send SMS Notification", MapEnableRights = PXCacheRights.Insert, MapViewRights = PXCacheRights.Select)]
    [PXProcessButton]
    public virtual IEnumerable sendSMSNotification(PXAdapter adapter)
    {
        ARInvoice invoice = PXCache<ARInvoice>.CreateCopy(Base.Document.Current);
        PXLongOperation.StartOperation(Base, delegate
        {
            ARInvoiceEntry invGraph = PXGraph.CreateInstance<ARInvoiceEntry>();
            invGraph.Document.Current = invoice;
            ARInvoiceEntryPXExt invGraphExt = invGraph.GetExtension<ARInvoiceEntryPXExt>();
            invGraphExt.SendTwilioNotification(invGraph, TwilioNotificationType.SMS);
        });
        return adapter.Get();
    }

    public PXAction<ARInvoice> SendCallNotification;

    [PXUIField(DisplayName = "Send Call Notification", MapEnableRights = PXCacheRights.Insert, MapViewRights = PXCacheRights.Select)]
    [PXButton(SpecialType = PXSpecialButtonType.Cancel)]
    public virtual IEnumerable sendCallNotification(PXAdapter adapter)
    {
        ARInvoice invoice = PXCache<ARInvoice>.CreateCopy(Base.Document.Current);
        PXLongOperation.StartOperation(Base, delegate
        {
            ARInvoiceEntry invGraph = PXGraph.CreateInstance<ARInvoiceEntry>();
            invGraph.Document.Current = invoice;
            ARInvoiceEntryPXExt invGraphExt = invGraph.GetExtension<ARInvoiceEntryPXExt>();
            invGraphExt.SendTwilioNotification(invGraph, TwilioNotificationType.OutBoundCall);
        });
        return adapter.Get();
    }

#endregion

Finally we included rules to control when to enable the actions in the ARInvoice_RowSelected event and we added the actions to the Actions menu.

public override void Initialize()
{
  TwilioNotificationSetup setup = TwilioSetupInfo.Current;

  Base.action.AddMenuAction(SendSMSNotification);
  Base.action.AddMenuAction(SendCallNotification);
}

AR30100 - Invoices and Memos

Processing screen

As the last part of the module, we built a simple processing screen. The processing screen was build as a new graph that shows all invoices with pending balance.

A custom DAC added as a processing filter allows the user to select the action (SMS or voice notification) and to filter documents by date, workgroup or owner.

The process delegate for each selected document just calls the SendTwilioNotification method with the selected action.

enter image description here

Conclusion

As we have seen, we were able to add very useful functionality to our ERP system in a few hours. By leveraging the capacities of Acumatica xRP and its Customization Framework we can offer powerful features to our customers in a shorter time by connecting to existing SaaS applications available right now.

Editors Note

The coding of the above integration was accomplished in only 4 hours! Take a moment to take that in to really appreciate, not only the power or the Acumatica xRP Platform – but also the skill and creativity of Marco and their team. You can see clearly, a very powerful combination.

For more information about Syntegh, please visit their website and Twitter feed.

Mark Franks

As a Platform Evangelist, Mark is responsible for showing people the specifics about what makes Acumatica’s Cloud Development Plaform wonderfully attractive to ISV & Partners. He's also passionate about Running, Latin, and his family. | E-mail: mfranks@acumatica.com | Skype: mfranks |

Subscribe to our bi-weekly newsletter

Subscribe