0

I would like to create a callout (webhook) when a custom field Batch_Name__c is updated which belongs to the Account Object.

The purpose of this callout is to send all account information to an external webhook (automation tool with no integration) only when the custom field is modified. (to reduce the number of calls and improve performances)

Using https://salesforce-webhook-creator.herokuapp.com/app, I created the following Apex Trigger (string url is changed)

trigger AccountTriggerWebhookTrigger on Account (after update) {

    String url = 'https://xxxxxxxx/AccountTrigger';

    String content = Webhook.jsonContent(Trigger.new, Trigger.old);

    Webhook.callout(url, content);

}

This trigger the external webhook well but in large volume so it's not scalable.

Thanks to the community (cc identigral), I explored a workaround using record-triggered Flow that should call the webhook thanks to invocable action.

The goal of invocable action is to use an apex class as an action as a result of the flow builder.

The https://salesforce-webhook-creator.herokuapp.com/app created two apex class.

@isTest
public class AccountTriggerWebhookTrigger {

    static SObject mock(String sobjectName) {
        SObjectType t = Schema.getGlobalDescribe().get(sobjectName);

        SObject o = t.newSobject();

        Map<String, Schema.SObjectField> m = t.getDescribe().fields.getMap();

        for (String fieldName : m.keySet()) {
            DescribeFieldResult f = m.get(fieldName).getDescribe();
            if (!f.isNillable() && f.isCreateable() && !f.isDefaultedOnCreate()) {
                if (f.getType() == DisplayType.Boolean) {
                    o.put(f.getName(), false);
                }
                else if (f.getType() == DisplayType.Currency) {
                    o.put(f.getName(), 0);
                }
                else if (f.getType() == DisplayType.Date) {
                    o.put(f.getName(), Date.today());
                }
                else if (f.getType() == DisplayType.DateTime) {
                    o.put(f.getName(), System.now());
                }
                else if (f.getType() == DisplayType.Double) {
                    o.put(f.getName(), 0.0);
                }
                else if (f.getType() == DisplayType.Email) {
                    o.put(f.getName(), '[email protected]');
                }
                else if (f.getType() == DisplayType.Integer) {
                    o.put(f.getName(), 0);
                }
                else if (f.getType() == DisplayType.Percent) {
                    o.put(f.getName(), 0);
                }
                else if (f.getType() == DisplayType.Phone) {
                    o.put(f.getName(), '555-555-1212');
                }
                else if (f.getType() == DisplayType.String) {
                    o.put(f.getName(), 'TEST');
                }
                else if (f.getType() == DisplayType.TextArea) {
                    o.put(f.getName(), 'TEST');
                }
                else if (f.getType() == DisplayType.Time) {
                    o.put(f.getName(), System.now().time());
                }
                else if (f.getType() == DisplayType.URL) {
                    o.put(f.getName(), 'http://foo.com');
                }
                else if (f.getType() == DisplayType.PickList) {
                    o.put(f.getName(), f.getPicklistValues()[0].getValue());
                }
            }
        }
        return o;
    }

    @isTest static void testTrigger() {
        SObject o = mock('Account');

        Test.startTest();
        insert o;
        update o;
        delete o;
        Test.stopTest();

        System.assertEquals(200, Webhook.response.getStatusCode());
        System.assertEquals('https://xxxxxxxx/AccountTrigger', Webhook.request.getEndpoint());

        if (Webhook.request != null) {
            Map<String, Object> jsonResponse = (Map<String, Object>) JSON.deserializeUntyped(Webhook.request.getBody());
            System.assertNotEquals(null, jsonResponse.get('userId'));
        }
    }

}

And

public class Webhook implements HttpCalloutMock {

    public static HttpRequest request;
    public static HttpResponse response;

    public HTTPResponse respond(HTTPRequest req) {
        request = req;
        response = new HttpResponse();
        response.setStatusCode(200);
        return response;
    }

    public static String jsonContent(List<Object> triggerNew, List<Object> triggerOld) {
        String newObjects = '[]';
        if (triggerNew != null) {
            newObjects = JSON.serialize(triggerNew);
        }

        String oldObjects = '[]';
        if (triggerOld != null) {
            oldObjects = JSON.serialize(triggerOld);
        }

        String userId = JSON.serialize(UserInfo.getUserId());

        String content = '{"new": ' + newObjects + ', "old": ' + oldObjects + ', "userId": ' + userId + '}';
        return content;
    }

    @future(callout=true)
    public static void callout(String url, String content) {

        if (Test.isRunningTest()) {
            Test.setMock(HttpCalloutMock.class, new Webhook());
        }

        Http h = new Http();

        HttpRequest req = new HttpRequest();
        req.setEndpoint(url);
        req.setMethod('POST');
        req.setHeader('Content-Type', 'application/json');
        req.setBody(content);

        h.send(req);
    }

}

I'm trying to apply the InvocableMethod (discussed here) and InvocableVariable interface (described here) but the documentation specify that

The data type of the invocable variable must be one of the following:

A primitive data type or a list of a primitive data type – the generic Object type is not supported.

An sObject type or a list of an sObject type – the generic sObject type is not supported.

I tried to use the InvocableMethod Annotation but it resulted in many syntax error and link to mock which I can't defined as global as required.

@isTest
public class BatchUpdateWebhookTriggerTest {
 @InvocableMethod(label='Test')

   global static SObject mock(String sobjectName) {

Error: Compile Error: Defining type for global methods must be declared as global at line 5 column 26   

Due to the many variables in the syntax of the ApexClass, the InvocableVariable Annotation seems to be a better fit with this ApexClass but it seems quite above my APEX skills.

I tried to find a callout flow template in AppsExchange but the potential solution are not free and it seems disproportionate for this use case.

Could you please advise me here to find a workaround ?

NB: I tried to find the solution by myself but it seems that I should learn APEX in order to not require your assistance, thanks for your time and understanding.

1 Answer 1

0

It's been a little while since you posted this, so it's possible you've already found some solutions. But from a fairly quick read on my part, I'm guessing you've confused a couple of different issues here.

The first thing is the test class you've included a snippet of above, which can't use the @InvocableMethod annotation. You'd only use that annotation on the method the Flow needs to run to do work Flow can't do. So the flow invokes the apex method (only visible to flow with @InvocableMethod), the test should exercise that method, but it doesn't need to worry about the annotation on it.

The second thing is related mocking for testing web callouts. Since you can't actually make the callout from a test class, you need a way to supply responses to your text class. Read below for the details on that, it's actually pretty simple once you see it:

dev guide page on testing callouts

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .