0

I have a service that is making callouts to another system and if it fails for some reason send email notifications. When I do the service for one record it is fine (e.g. call it from a trigger). However there is also a batch class that runs for daily sync that calls it, and when it hits an error and sends an email everything after that dies out with You have uncommitted work pending. Please commit or rollback before calling out

Is this an issue of how I'm sending emails in the batch?

Batch class:

global class SyncWithSomeSystem implements Database.Batchable<SObject>, Database.AllowsCallouts, Schedulable {

    public void execute(SchedulableContext sc) {
        Database.executeBatch(new SyncWithSomeSystem(), 50);
    }

    global Database.QueryLocator start(Database.BatchableContext BC) {
        return Database.getQueryLocator([Some querry]);
    }

    global void execute(Database.BatchableContext BC, List<Contract__c> scope) {

        SomeSystemService service = new SomeSystemService();

        for (Contract__c contract : scope) {
            service.handleSubscription(contract.Id);
        }
    }

    global void finish(Database.BatchableContext BC) {}

}

Service class:

public class SomeSystemService {

    public void handleSubscription(Id contractId) {
        try {
            List<ContractLineItem__c> lineItems = this.getLineItems(contractId);
            if(lineItems.isEmpty()) { return; }

            Contact contact = this.getContact(lineItems[0].Contract__r.Account__c);

            for(ContractLineItem__c lineItem : lineItems) {
                Subscription subcription = new Subscription(lineItem, contact);
                List<Subscription> existingSubscriptions = this.getSubscriptions(contact, subcription.plan_currency);

                if (existingSubscriptions.isEmpty()) {
                    this.createSubscription(subcription);
                } else if (!this.subscriptionExists(subcription, existingSubscriptions)) {
                    this.updateSubscription(subcription);
                }

            }
        } catch (SomeSystemServiceException e) {
            this.emailNotification(e);
        }
    }

    private Boolean subscriptionExists(Subscription subscription, List<Subscription> existingSubscriptions) {
        for(Subscription existing : existingSubscriptions) {
            if(existing.effective_date == subscription.effective_date &&
               existing.value == subscription.value) {
                   return true;
            }
        }

        return false;
    }

    private List<Subscription> getSubscriptions(Contact contact, string currencyIsoCode) {
//GET callout
    }

    private void createSubscription(Subscription request) {
//POST Callout
    }

    private void updateSubscription(Subscription request) {
//PUT Callout
    }

    public void emailNotification(SomeSystemServiceException error) {
        Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
        String[] toAddresses = new String[] { SomeSystemSettingsUtil.errorEmailRecipient() };
        mail.setToAddresses(toAddresses);
        mail.setSenderDisplayName('Salesforce SomeSystem Integration');

        mail.setSubject('Creating subscription failed');

        String messageBody = 'Could not create a subscription for Contract.\n';
        messageBody += '\n\nError details:';
        messageBody += '\n' + error.getMessage();
        mail.setPlainTextBody(messageBody);

        this.sendEmail(mail);
    }

    public void sendEmail(Messaging.SingleEmailMessage mail) {
        try {
            Messaging.reserveSingleEmailCapacity(1);
            Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
        } catch (NoAccessException e) {
            System.debug('could not send email: ' + e.getMessage());
        }
    }

    public class SomeSystemServiceException extends Exception { }
}
0

Rather than immediately sending the email notifications, make the batch stateful, collect the required notification details into some state in your batch (e.g. a list of the exception messages) and send all the notifications in one go during the finish method processing. That way you separate the processing of your callouts from the sending of emails and avoid this Salesforce processing flow limitation.

  • Oh I see, I did try to collect them out of the for loop but inside each job and it was still breaking, I guess I should have gone for the finish. I will try that thank you. – Iojo Jun 27 at 10:20
  • Just remember to make the batchable stateful so you can maintain this state between the various execute calls and the finish call. This could even let you send just the one notification email with all the errors listed. (You might like to add the failing object IDs into the email body too.) – Phil W Jun 27 at 10:26
  • execute() could also defer emails to a queueable provided it is the only async child spawned by execute() – cropredy Jun 27 at 18:42

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.