[SalesForce] DML update in @future callout not updating sObject

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);
            }
        }
    }

}

Best Answer

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();
    }
}
Related Topic