4

I've got a Apex class that takes the data from an Account object and uses it to create an account in my company's database by hitting an API. The API accepts JSON then inserts all the appropriate records into our database and returns the company's entityID and the username of the admin and then updates the Account sObject in Salesforce with those two fields. The account creation part works fine, but the DML update does not. I've tried it in both a before insert/update trigger and an after insert/update, but it never works. Now, whenever I execute the future method anonymously in Eclipse, the sObject updates successfully. I've tried to follow the scheme in this example.


My code is as follows:

Trigger

trigger CreateFleet on Account (after insert, after update) {
    List<Account> accountList = Trigger.new;

    for (Account account : accountList) {
        CreateFleetHandler handler = new CreateFleetHandler(account);
        handler.handleCreateFleet();
    }
}

Handler

public class CreateFleetHandler {
    private final String ACCOUNT_NAME_ERROR = 'This company name is already in the database. Please check if the account has already been created.';
    private final Account account;

    public CreateFleetHandler(Account account) {
        this.account = account;
    }

    public void handleCreateFleet(){
        if (account.Type == PrepareFleetData.CUSTOMER_COMMERCIAL_PICKLIST_VALUE) {
            PrepareFleetData prepare = new PrepareFleetData(account);
            prepare.getContactList();

            List<String> errors = new List<String>();
            prepare.checkRequiredFields();
            errors = prepare.checkContact(errors);
            if (!errors.isEmpty()) {
                this.setErrors(errors);
            }
            if (errors.isEmpty()) {
                this.createFleet(prepare);
            }
        }
    }

    private void createFleet(PrepareFleetData prepare){
        String jsonData = prepare.setFleetData();
        CreateFleet.createNewFleet(jsonData, String.valueOf(this.account.Id));
    }

    private void setErrors(List<String> errors) {
        for (String e : errors) {
            if (e == this.ACCOUNT_NAME_ERROR) {
                Account.Name.addError(e);
            }
            else {
                account.addError(e);
            }
        }
    }
}

Validation Class

public class PrepareFleetData {

    public static final String CUSTOMER_COMMERCIAL_PICKLIST_VALUE = 'Customer - Commercial';
    private final String CUSTOMER_COMMERICAL_POST_VALUE = '1';
    private final String CONTACT_ROLE_ADMIN = 'Project Implementation Manager (Admin)';
    private final String CONTACT_ROLE_PRIMARY_TECHNICAL = 'Primary Technical';

    private Account account;
    private Contact contact;
    private FleetData fleetData;

    public PrepareFleetData(Account account, Contact contact){
        this.account = account;
        this.contact = contact;
        this.fleetData = new FleetData();
    }

    public PrepareFleetData(Account account) {
        this.account = account;
        this.fleetData = new FleetData();
    }

    public FleetData getFleetData() {
        return this.fleetData;
    }

    public String setFleetData() {
        Account account = this.account;
        Contact contact = this.contact;

        this.fleetData.companyName = account.Name;
        this.fleetData.maxPhones = (Integer)account.Total_Fleet_Size__c;
        this.fleetData.maxDevices = (Integer)account.Total_Fleet_Size__c;
        this.fleetData.phoneAmount = 0;
        this.fleetData.deviceAmount = 0;
        this.fleetData.city = account.BillingCity;
        this.fleetData.state = account.BillingState;
        this.fleetData.country = account.BillingCountry;
        this.fleetData.address = account.BillingStreet;
        this.fleetData.zip = account.BillingPostalCode;
        this.fleetData.phone = account.Phone;
        this.fleetData.fax = account.Fax;
        this.fleetData.url = account.WebSite;
        this.fleetData.email = contact.Email;
        this.fleetData.notes = account.Description;
        this.fleetData.channel = account.Channel__c;
        this.fleetData.firstName = contact.FirstName;
        this.fleetData.lastName = contact.LastName;
        this.fleetData.poNumber = 11111;
        this.fleetData.accountType = this.convertAccountType(account.Type);
        this.fleetData.disableTelematics = account.Disable_Telematics__c;
        this.fleetData.isDriveUp = account.DriveUp_Fleet__c;

        return this.setJSON(this.fleetData);
    }

    public void checkRequiredFields() {
        Account account = this.account;

        if (account.Total_Fleet_Size__c == null) {
            account.Total_Fleet_Size__c.addError('Total Fleet Size is not set.');
        }
    }

    public List<String> checkContact(List<String> errors) {
        Contact contact = this.contact;

        if (contact == null) {
            List<String> roles = new String[]{
                this.CONTACT_ROLE_ADMIN,
                this.CONTACT_ROLE_PRIMARY_TECHNICAL
            };
            String errorString = 'There is no contact assigned with the role {0} or {1}';
            errors.add(String.format(errorString, roles));
            return errors;
        }       
        if (String.isBlank(contact.FirstName)) {
            errors.add('Contact\'s First Name is not set.');
        }
        if (String.isBlank(contact.LastName)) {
            errors.add('Contact\'s Last Name is not set.');
        }
        if (String.isBlank(contact.Email)) {
            errors.add('Contact\'s Email is not set');
        }

        return errors;
    }

    public void getContactList() {
        Account account = this.account;

        List<Contact> contactList = [
            SELECT FirstName,
                   LastName, 
                   Email,
                   Contact_Role__c,
                   CreatedDate
            FROM Contact
            WHERE AccountId = :account.Id
        ];

        this.contact = this.setContact(contactList);
    }

    private Contact setContact(List<Contact> contactList) {
        List<Contact> technicalContactList = new List<Contact>();

        for (Contact con : contactList) {
            if (con.Contact_Role__c == this.CONTACT_ROLE_ADMIN) {
                return con;
            }
            if (con.Contact_Role__c == this.CONTACT_ROLE_PRIMARY_TECHNICAL) {
                technicalContactlist.add(con);
            }
        }

        if (!technicalContactList.isEmpty()) {
            DateTime oldestDate = DateTime.now();
            Contact oldestContact = null;
            for (Contact con : technicalContactList) {
                if (con.CreatedDate < oldestDate) {
                    oldestDate = con.CreatedDate;
                    oldestContact = con;
                }
            }
            return oldestContact;
        }

        return null;
    }

    private String setJSON(FleetData fleetData) {
        if (fleetData != null) {
            String jsonData = JSON.serialize(fleetData);
            return jsonData;
        }
        return null;
    }

    private String convertAccountType(String value) {
        if (value == PrepareFleetData.CUSTOMER_COMMERCIAL_PICKLIST_VALUE) {
            return this.CUSTOMER_COMMERICAL_POST_VALUE;
        }
        return this.CUSTOMER_COMMERICAL_POST_VALUE;
    }
}

Callout Class

public class CreateFleet {

    private static final Integer HTTP_OK = 200;
    private static final Integer HTTP_UNPROCESSABLE_ENTITY = 422;
    private static final String url = 'URL';

    @future (callout=true)
    public static void createNewFleet(String jsonData, String accountID) {
        System.debug('creating');
        if (jsonData != null) {
            Router router = new Router();
            String jsonResponse = router.postJSON(CreateFleet.url, jsonData);
            Integer status = Router.getStatusCode();

            if (status == CreateFleet.HTTP_OK) {
                ParseResponse response = ParseResponse.parse(jsonResponse);
                Account acc = [
                    SELECT EntityID__c,
                           Username__c
                    FROM Account 
                    Where Id = :accountID
                ];
                acc.EntityID__c = response.entityID;
                acc.Username__c = response.username;
                System.debug(acc);

                /**
                * This is the part that isn't working.
                */
                update acc;
            }
            if (status == CreateFleet.HTTP_UNPROCESSABLE_ENTITY) {
                ParseResponseWithErrors response = ParseResponseWithErrors.parse(jsonResponse);
                List<String> errors = response.errors;
                System.debug(errors);
            }
        }
    }

}
  • 4
    @future methods (I think this might go, to some extent, for all asynchronous execution) generally don't report errors or exceptions unless you have code that explicitly handles error notification. Without that, the only way I know of determining what went wrong (and not just that something went wrong) is to open up the Developer Console and watch the logs tab (at the bottom) for a 'Future Handler' operation. That may give you a better idea of what's going wrong. – Derek F Apr 25 '16 at 16:06
5

You are expecting from your trigger next flow:

insert account->future callout->update account

But in your code after update statement, you are executing update DML in future context, which fires trigger again to make callout in future ... SF does not allow to call future from future, that's why update doesn't work.

@Derek F is right, future exception does not appear at UI, since it's different context. If you open developer console, you will see that futurehandler has twrown something like:

System.AsyncException: Future method cannot be called from a future or batch method: ...

Try to check context, it will help to execute trigger only for first DML

public class CreateFleetHandler {
//...

public void handleCreateFleet(){
    if (!System.isFuture()) {

        if (account.Type == PrepareFleetData.CUSTOMER_COMMERCIAL_PICKLIST_VALUE) {
            //...
        }
    }
}
//...
}

Or/And you can use additional check in trigger itself:

for (Account account : accountList) {
    if (account.externalId == Null) {
        CreateFleetHandler handler = new CreateFleetHandler(account);
        handler.handleCreateFleet();
    }
}
| improve this answer | |
  • 1
    If you open developer console, you could see something like: Future method cannot be called from a future or batch method: ... – Ilya Lepesh Apr 25 '16 at 16:57
  • Figured it out, the future call was updating the Account object which called the trigger again and in turn called the future call again, which caused the whole salesforce side of the operation to fail. If not for the future calling future error, it would have been an infinite loop. Please update your answer to explain this more clearly if you don't mind. I didn't quite get what you were saying until I looked at the logs in the developer console(which I didn't know you could do, lol. I'm a noob.). – Allie Fitter Apr 25 '16 at 18:26
  • Debugging is very important, (try this)[developer.salesforce.com/docs/atlas.en-us.eclipse.meta/eclipse/… – Ilya Lepesh Apr 25 '16 at 19:06
  • Thanks a lot for all the help! This was a head scratcher without knowing where to go for logs. – Allie Fitter Apr 25 '16 at 19:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.