[SalesForce] Call @future from @RemoteAction

Can you call a @future method from a @RemoteAction method?

I've got an installed package that runs a VFRemoting and they allow me to write a plugin for part of the process. The issue is, I'd like to run something after the remoting is completed, but can't seem to be able to. I can get it to call any method I want except one labeled @future since it doesn't seem to actually process… Any way I can force something to run when the @RemoteAction is complete without having access to the code itself other than a plugin that falls mid-sequence? @future when compared to standard apex is exactly the type of functionality I'm after here, but that doesn't seem to work in this instance…

global class SBQQCalculatorPlugin implements SBQQ.QuoteCalculatorPlugin {

    /*
        Quote is calculated when either of the following occurs: 
        + User clicks on Edit Lines button (Line Editor is loaded). 
        + User clicks on Calculate button in Line Editor. 
        + User adds products in Line Editor. 
        + User deletes lines in Line Editor. 
        + User clicks Save in Line Editor. 
        + User edits quote header and changes one of the fields that trigger recalc. 
        + User edits quote line using the native Edit button. 

        The following sequence of events takes place during each Calculate cycle:
        1. QuoteCalculatorPlugin is initialized (if configured) and onInit() method is called.
        2. Transaction savepoint is set.
        3. Quote record is upserted.
        4. Quote Line records are reloaded (upserted and queried) to evaluate formula fields used in price rules.
        5. QuoteCalculatorPlugin.onBeforeCalculate() method is called with original (unflushed) quote lines.
        6. Quantities are calculated (component quantities, batch quantities, etc.)
        7. QuoteCalculatorPlugin.onBeforePriceRules() method is called with original (unflushed) quote lines.
        8. Price rules are evaluated against reloaded (flushed) quote lines.
        9. QuoteCalculatorPlugin.onAfterPriceRules() method is called with original (unflushed) quote lines.
        10. Native calculation logic is executed.
        11. QuoteCalculatorPlugin.onAfterCalculate() method is called with original (unflushed) quote lines.
        12. Quote Line records are reloaded again to evaluate formula fields changed as a result of above processing.
        13. Transaction is rolled back.

        NOTE: To detect upserts of Quote Lines that will later be rolled back you can examine SBQQ__Incomplete__c field which is set to TRUE during rolled back transaction.
    */

    SBQQCalculatorHelper helper = new SBQQCalculatorHelper();

    global void onInit(List<sObject> lines) {
        helper.setMapValues();
    }

    global void onBeforeCalculate(sObject quote, List<sObject> lines) {
        helper.setLineValuesList(quote.Id, (List<SBQQ__QuoteLine__c>)lines, true);
    }

    global void onBeforePriceRules(sObject quote, List<sObject> lines) {}

    global void onAfterPriceRules(sObject quote, List<sObject> lines) {}

    global void onAfterCalculate(sObject quote, List<sObject> lines) {
        helper.setLineValuesNet(quote.Id, (List<SBQQ__QuoteLine__c>)lines, true);
    }

}

… and here's the helper class that I defer the logic to… I've resorted to also calling this same class from my before triggers and it solved my issue, albiet less ideal than I'd hoped…

global with sharing class SBQQCalculatorHelper {

    private static Map<String, Map<String, Decimal>> mapValues = new Map<String, Map<String, Decimal>>();

    private static SBQQ__Quote__c qt;
    private static Map<Id, SBQQ__QuoteLine__c> ql;

    global void setMapValues() {

        mapValues.put('1', new Map<String, Decimal>());
        mapValues.put('2', new Map<String, Decimal>());
        mapValues.put('3', new Map<String, Decimal>());

        mapValues.get('1').put('listSubs', 0);
        mapValues.get('2').put('listSubs', 0);
        mapValues.get('3').put('listSubs', 0);

        mapValues.get('1').put('net', 0);
        mapValues.get('2').put('net', 0);
        mapValues.get('3').put('net', 0);

    }

    private static void getLatestData(Id quoteId, List<SBQQ__QuoteLine__c> lines) {

        qt = [SELECT Id, Months__c, MonthsMod__c, Backloaded__c, SBQQ__CustomerDiscount__c FROM SBQQ__Quote__c WHERE Id = :quoteId];
        ql = new Map<Id, SBQQ__QuoteLine__c>([SELECT Id, SBQQ__Product__c, SBQQ__EffectiveQuantity__c, GlobalDiscount__c FROM SBQQ__QuoteLine__c WHERE Id IN :lines]);

    }

    global void setLineValuesList(Id quoteId, List<SBQQ__QuoteLine__c> lines, Boolean isCalculator) {

        getLatestData(quoteId, lines);

        for (SBQQ__QuoteLine__c line : lines) {
            if (line.SBQQ__ProductFamily__c == 'Subscriptions' && (line.ProductType__c != 'Support' || line.SBQQ__SubscriptionPercent__c == null)) {
                mapValues.get('1').put('listSubs', mapValues.get('1').get('listSubs') + (Decimal)setListTotal(line, '1'));
                mapValues.get('2').put('listSubs', mapValues.get('2').get('listSubs') + (Decimal)setListTotal(line, '2'));
                mapValues.get('3').put('listSubs', mapValues.get('3').get('listSubs') + (Decimal)setListTotal(line, '3'));
            }
        }

        for (SBQQ__QuoteLine__c line : lines) {
            line.ListTotalYear1__c = setListTotal(line, '1');
            if (UtilGeneral.isDebug)
                System.debug('### line.ListTotalYear1__c = ' + line.ListTotalYear1__c);
            line.ListTotalYear2__c = setListTotal(line, '2');
            if (UtilGeneral.isDebug)
                System.debug('### line.ListTotalYear2__c = ' + line.ListTotalYear2__c);
            line.ListTotalYear3__c = setListTotal(line, '3');
            if (UtilGeneral.isDebug)
                System.debug('### line.ListTotalYear3__c = ' + line.ListTotalYear3__c);
            if (isCalculator) {
                line.SBQQ__Quantity__c = setQuantity(line);
                if (UtilGeneral.isDebug)
                    System.debug('### line.SBQQ__Quantity__c = ' + line.SBQQ__Quantity__c);
            }
        }

    }

    global void setLineValuesNet(Id quoteId, List<SBQQ__QuoteLine__c> lines, Boolean isCalculator) {

        getLatestData(quoteId, lines);

        for (SBQQ__QuoteLine__c line : lines) {
            if (!ql.get(line.Id).GlobalDiscount__c) {
                mapValues.get('1').put('net', mapValues.get('1').get('net') + (Decimal)setNetTotal(line, '1'));
                mapValues.get('2').put('net', mapValues.get('2').get('net') + (Decimal)setNetTotal(line, '2'));
                mapValues.get('3').put('net', mapValues.get('3').get('net') + (Decimal)setNetTotal(line, '3'));
            }
        }

        for (SBQQ__QuoteLine__c line : lines) {
            line.NetTotalYear1__c = setNetTotal(line, '1');
            if (UtilGeneral.isDebug)
                System.debug('### line.NetTotalYear1__c = ' + line.NetTotalYear1__c);
            line.NetTotalYear2__c = setNetTotal(line, '2');
            if (UtilGeneral.isDebug)
                System.debug('### line.NetTotalYear2__c = ' + line.NetTotalYear2__c);
            line.NetTotalYear3__c = setNetTotal(line, '3');
            if (UtilGeneral.isDebug)
                System.debug('### line.NetTotalYear3__c = ' + line.NetTotalYear3__c);
            line.NetSubtotalYear1__c = setSubtotalAmount(line, 'NetTotalYear1__c');
            if (UtilGeneral.isDebug)
                System.debug('### line.NetSubtotalYear1__c = ' + line.NetSubtotalYear1__c);
            line.NetSubtotalYear2__c = setSubtotalAmount(line, 'NetTotalYear2__c');
            if (UtilGeneral.isDebug)
                System.debug('### line.NetSubtotalYear2__c = ' + line.NetSubtotalYear2__c);
            line.NetSubtotalYear3__c = setSubtotalAmount(line, 'NetTotalYear3__c');
            if (UtilGeneral.isDebug)
                System.debug('### line.NetSubtotalYear3__c = ' + line.NetSubtotalYear3__c);
            if (isCalculator) {
                line.SBQQ__AdditionalDiscountAmount__c = setDiscountAmount(line);
                if (UtilGeneral.isDebug)
                    System.debug('### line.SBQQ__AdditionalDiscountAmount__c = ' + line.SBQQ__AdditionalDiscountAmount__c);
            }
        }

    }

    private static Decimal setListTotal(SBQQ__QuoteLine__c line, String year) {
        if (line.ProductType__c == 'Support' && line.SBQQ__SubscriptionPercent__c != null) {
            return mapValues.get(year).get('listSubs') * line.SBQQ__SubscriptionPercent__c / 100;
        } else {
            if ((year == '2' && qt.Months__c <= 12) || (year == '3' && qt.Months__c <= 24))
                return 0;
            Decimal months;
            if ((year == '1' && (qt.Months__c <= 12 || qt.Backloaded__c)) || (year == '2' && (qt.Months__c > 12 && qt.Months__c <= 24 && !qt.Backloaded__c)) || (year == '3' && (qt.Months__c > 24 && !qt.Backloaded__c))) {
                months = qt.MonthsMod__c;
            } else {
                months = 12;
            }
            return line.SBQQ__ListPrice__c * (Decimal)((line.get('QuantityYear' + year + '__c') == null) ? 0 : line.get('QuantityYear' + year + '__c')) * months / 12;
        }
    }

    private static Decimal setNetTotal(SBQQ__QuoteLine__c line, String year) {
        Decimal disc = (line.get('DiscountYear' + year + '__c') == null) ? 0 : (Decimal)line.get('DiscountYear' + year + '__c');
        if (ql.get(line.Id).GlobalDiscount__c) {
            return mapValues.get(year).get('net') * -1 * disc / 100;
        } else {
            disc = (disc == 0) ? ((qt.SBQQ__CustomerDiscount__c == null) ? 0 : qt.SBQQ__CustomerDiscount__c) : disc;
            return (Decimal)line.get('ListTotalYear' + year + '__c') * (100 - disc) / 100;
        }
    }

    private static Decimal setSubtotalAmount(SBQQ__QuoteLine__c line, String obj) { // TODO: ensure all the datapoints are available!!!
        if (ql.get(line.Id).GlobalDiscount__c)
            return 0;
        return (Decimal)line.get(obj);
    }

    private static Decimal setQuantity(SBQQ__QuoteLine__c line) { // TODO: ensure all the datapoints are available!!!
        if (ql.get(line.Id).GlobalDiscount__c) {
            return 1;
        } else if (line.SBQQ__ListPrice__c != 0) {
            return (line.ListTotalYear1__c + line.ListTotalYear2__c + line.ListTotalYear3__c) * 12 / line.SBQQ__ListPrice__c / ((line.SBQQ__ProductFamily__c == 'Subscriptions' || line.ProductType__c == 'Support') ? qt.Months__c : 12);
        }
        return 1;
    }

    private static Decimal setDiscountAmount(SBQQ__QuoteLine__c line) { // TODO: ensure all the datapoints are available!!!
        if (ql.get(line.Id).GlobalDiscount__c) {
            return line.ListTotalYear1__c + line.ListTotalYear2__c + line.ListTotalYear3__c - line.NetTotalYear1__c - line.NetTotalYear2__c - line.NetTotalYear3__c;
        } else if ((line.ListTotalYear1__c + line.ListTotalYear2__c + line.ListTotalYear3__c) != 0 && ql.get(line.Id).SBQQ__EffectiveQuantity__c != 0) {
            return (line.ListTotalYear1__c + line.ListTotalYear2__c + line.ListTotalYear3__c - line.NetTotalYear1__c - line.NetTotalYear2__c - line.NetTotalYear3__c) / ql.get(line.Id).SBQQ__EffectiveQuantity__c;
        }
        return 0;
    }

}

Best Answer

Yes.

Could an exception, or the transaction rollback in Calculate cycle 13 be undoing the @Future?

The below works alright, so it sounds like the invocation of the plugin may be wanting.

RemoteController.cls

public class RemoteController {

    @RemoteAction static public void someAction() {
        RemoteController.deferStuff();
    }

    @Future static public void deferStuff() {
        insert new Account(Name = 'Future');
    }

}

RemotePage.page

<apex:page controller="RemoteController">
    <apex:outputLink onclick="RemoteController.someAction(function() {});">
        someAction()
    </apex:outputLink>
</apex:page>
Related Topic