We are using a webhook in our plugin to send a URL to customers for online approval. Upon clicking the URL, the page loads, accepts a signature, and attaches it to the relevant document.
This is a simple sample code I have used for testing in Acumatica 24R2 and the response is empty
public class TestWebhook : IWebhookHandler
{ private NameValueCollection _queryParameters;
private const string cLocationCD = "KeyValues";
private const string cMode = "Mode";
private const string cSendMode = "SendNotification";
private const string cGetMode = "ReceiveResponse";
//async Task<IHttpActionResult> IWebhookHandler.ProcessRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken)
public async Task HandleAsync(WebhookContext context, CancellationToken cancellation)
{
using (var scope = GetUserScope())
{
//_queryParameters = HttpUtility.ParseQueryString(request.RequestUri.Query);
//_queryParameters = HttpUtility.ParseQueryString(context.Request.Query.ToString());
string htmlResponse = string.Empty;
if (!context.Request.Query.ContainsKey(cLocationCD))
throw new Exception($"The {cLocationCD} Parameter was not specified in the Query String");
IReadOnlyDictionary<string, Microsoft.Extensions.Primitives.StringValues> keycollection = context.Request.Query;
var collectorID = context.Request.Query[cLocationCD];
try
{
string sMode = string.Empty;
string values = collectorID;
string[] arr = values.Split(new char[] { ';' });
string trantype = arr[0];
string doctype = arr[1];
string docnbr = arr[2];
if (!keycollection.ContainsKey(cMode))
{
//if nothing is specified for a mode Parameter we will assume GetSurvey
sMode = "SendNotification";
}
else
{
sMode = context.Request.Query[cMode];
}
switch (sMode)
{
case cSendMode:
htmlResponse = GetSendAuth(trantype, doctype, docnbr);
break;
case cGetMode:
htmlResponse = SubmitApproval(collectorID, context.Response);
break;
default:
//htmlResponse = ReturnModeNotRecognized(sMode);
break;
}
}
catch (PXException e)
{
throw e;
}
StreamWriter writer = new StreamWriter(context.Response.Body);
writer.Write(htmlResponse);
writer.Close();
//HtmlActionResult htmlaction = new HtmlActionResult(htmlResponse);
//return htmlac;
}
}
private IDisposable GetUserScope()
{
//todo: For now we will use admin but we will want to throttle back to a
// user with restricted access as to reduce any risk of attack.
// perhaps this can be configured in the Surveys Preferences/Setup page.
var userName = "admin";
if (PXDatabase.Companies.Length > 0)
{
var company = PXAccess.GetCompanyName();
if (string.IsNullOrEmpty(company))
{
company = PXDatabase.Companies[0];
}
userName = userName + "@" + company;
}
return new PXLoginScope(userName);
}
private string GetSendAuth(string trantype, string doctype, string docnbr)
{
string _retval = String.Empty;
WebHookListener g = PXGraph.CreateInstance<WebHookListener>();
string listningEndPoint = g.GetWekHooksUrl();
string encrypedval = $"{trantype};{doctype};{docnbr}";
listningEndPoint = $"{listningEndPoint}?{cLocationCD}={encrypedval}&{cMode}={cGetMode}";
Tuple<string, string, string> label = GetLabel(trantype, doctype, docnbr);
Tuple<string, string, string> value = GetValues(trantype, doctype, docnbr);
StringBuilder builder = new StringBuilder();
builder.AppendLine("<!DOCTYPE html>");
builder.AppendLine("<html xmlns=\"http://www.w3.org/1999/xhtml\">");
builder.AppendLine("<head runat=\"server\">");
builder.AppendLine("<title>WebHook Test</title>");
builder.AppendLine("<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\" />");
builder.AppendLine("</head>");
builder.AppendLine("<body>");
builder.AppendLine("<header><h2>Webhook Test</h2></header>");
builder.AppendLine("<nav class=\"labels\">");
builder.AppendLine($"<label>{label.Item1} :</label> <label>{value.Item1}</label> <br> <label>{label.Item2} :</label> <label>{value.Item2}</label> <br> <label>{label.Item3} :</label><label>{value.Item3}</label> <br>");
builder.AppendLine("</nav>");
builder.AppendLine("<section>");
builder.AppendLine($"<form id=\"form1\" runat=\"server\" class=\"sigPad\" style=\"width:100%; height:100%; position:relative; \" action= \"{listningEndPoint}\" method=\"post\" >");
builder.AppendLine("<table width=\"100%\" cellpadding=\"2\" cellspacing=\"2\" border=\"0\">");
builder.AppendLine("<tr><td><label for=\"myCheckbox\"><input type=\"checkbox\" id=\"cbAuthorize\" name=\"cbAuthorize\" runat=\"server\" />I agree and approve</label></td></tr>");
builder.AppendLine("<tr><td><br><label for=\"name\"><b>Click button to submit</b></label><p><input type =\"submit\" value =\"Submit\"></p></td><td><div><img id=\"SigImg\" src=\"\" runat=\"server\" /></div></td></tr>");
builder.AppendLine("</table>");
builder.AppendLine("</form>");
builder.AppendLine("</section>");
builder.AppendLine("<footer>WebHook Test</footer>");
builder.AppendLine("</body>");
builder.AppendLine("</html>");
_retval = builder.ToString();
return _retval;
}
//private string SubmitApproval(string collectorID, HttpRequestMessage request)
private string SubmitApproval(string collectorID, PX.Api.Webhooks.WebhookResponse request)
{
string body = string.Empty;
Stream stream = request.Body;
stream.Position = 0;
using (StreamReader reader = new StreamReader(stream))
{
// Read the entire stream to a string
body = reader.ReadToEnd();
}
string values = collectorID;
string[] arr = values.Split(new char[] { ';' });
string trantype = arr[0];
string doctype = arr[1];
string docnbr = arr[2];
bool authorize = false;
arr = body.Split(new char[] { '&' });
foreach (string node in arr)
{
string[] element = node.Split(new char[] { '=' });
switch (element[0].ToLower())
{
case "cbAuthorize": authorize = (element[1].ToLower() == "on"); break;
default: break;
}
}
SOOrderEntry g = PXGraph.CreateInstance<SOOrderEntry>();
SOOrder ord = SOOrder.PK.Find(g, doctype, docnbr);
if(ord != null)
{
WHSOOrderExt ext = ord.GetExtension<WHSOOrderExt>();
ext.UsrApprove = authorize;
ext.UsrHits = (ext.UsrHits ?? 0) + 1;
g.Document.Update(ord);
g.Save.Press();
}
var view = @"
<!DOCTYPE html>
<html>
<body>
<h1>Approval</h1>
Thank You Your Submitted your answer was {0}
</body>
</html>
";
return string.Format(view,((authorize)?"Yes":"No"));
}
private Tuple<string, string, string> GetLabel(string trantype, string doctype, string docnbr)
{
Tuple<string, string, string> _retval = new Tuple<string, string, string>("", "", "");
_retval = new Tuple<string, string, string>("Shipment # : ",
"Customer Name: ",
"Contact # : ");
return _retval;
}
private Tuple<string, string, string> GetValues(string trantype, string doctype, string docnbr)
{
Tuple<string, string, string> _retval = new Tuple<string, string, string>("", "", "");
string[] val = GetSalesOrderLabelDetail(doctype, docnbr);
_retval = new Tuple<string, string, string>(val[0], val[1], val[2]);
return _retval;
}
private string[] GetSalesOrderLabelDetail(string type, string refnbr)
{
string[] _return = new string[] { "", "", "" };
SOOrderEntry grp = PXGraph.CreateInstance<SOOrderEntry>();
grp.Document.Current = PXSelect<SOOrder, Where<SOOrder.orderType, Equal<Required<SOOrder.orderType>>, And<SOOrder.orderNbr, Equal<Required<SOOrder.orderNbr>>>>>.Select(grp, type, refnbr);
if (grp.Document.Current != null)
{
List<string> arr = new List<string>();
arr.Add(grp.Document.Current.OrderNbr);
if (grp.customer.Current != null)
arr.Add($"{grp.customer.Current.AcctCD}-{grp.customer.Current.AcctName}");
else
arr.Add(" ");
if (grp.customer.Current != null)
{
Contact contact = PXSelect<Contact, Where<Contact.bAccountID, Equal<Required<Contact.bAccountID>>, And<Contact.contactID, Equal<Required<Contact.contactID>>>>>.Select(grp, grp.customer.Current.BAccountID, grp.customer.Current.DefBillContactID);
if (contact != null)
arr.Add(contact.Phone1 ?? string.Empty);
else
arr.Add(" ");
}
_return = arr.ToArray();
}
return _return;
}
}
The debugging code give the following error.
I raised a support case with Acumatica and received a solution to fix the issue.
Acumatica recommended passing webhookRequest instead of webhookResponse in SubmitApproval to achieve the desired result.
**private string SubmitApproval(string collectorID, PX.Api.Webhooks.WebhookRequest request)**
{
string body = string.Empty;
Stream stream = request.Body;
stream.Position = 0;
using (StreamReader reader = new StreamReader(stream))
{
// Read the entire stream to a string
body = reader.ReadToEnd();
}
string values = collectorID;
string[] arr = values.Split(new char[] { ';' });
string trantype = arr[0];
string doctype = arr[1];
string docnbr = arr[2];
bool authorize = false;
arr = body.Split(new char[] { '&' });
foreach (string node in arr)
{
string[] element = node.Split(new char[] { '=' });
switch (element[0].ToLower())
{
case "cbauthorize": authorize = (element[1].ToLower() == "on"); break;
default: break;
}
}
SOOrderEntry g = PXGraph.CreateInstance<SOOrderEntry>();
SOOrder ord = SOOrder.PK.Find(g, doctype, docnbr);
if(ord != null)
{
WHSOOrderExt ext = ord.GetExtension<WHSOOrderExt>();
ext.UsrApprove = authorize;
ext.UsrHits = (ext.UsrHits ?? 0) + 1;
g.Document.Update(ord);
g.Save.Press();
}
var view = @"
<!DOCTYPE html>
<html>
<body>
<h1>Approval</h1>
Thank You Your Submitted your answer was {0}
</body>
</html>
";
return string.Format(view,((authorize)?"Yes":"No"));
}