3

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

}
  • How do they call the plugin and what does it look like? – bigassforce Nov 22 '14 at 0:45
  • 1
    Sure. It's part of the SteelBrick CPQ package. I've modified my OP. – Nathan Williams Nov 22 '14 at 18:11
  • 1
    Originally, I'd tried to re-run the helper.setMapValues(), helper.setLineValuesList(), and helper.setLineValuesNet() methods in an @future call at the end of the plugin because their code doesn't save the values I "put" throughout the procedure. I settled for calling them in my before triggers, which was less elegant, but works... – Nathan Williams Nov 22 '14 at 18:15
  • 1
    Could an exception, or the transaction rollback in #13 be undoing the @Future? – bigassforce Nov 23 '14 at 0:52
  • 1
    interesting idea, and i suppose that's quite possible... they would then have the data they needed stored, so no need to run the logic through again, thus not firing the @future in a context where it would stick... i'll buy it! – Nathan Williams Nov 23 '14 at 1:19
4

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>
  • 1
    Hmm, thanks for digging in. Yes, there must be something funky with the way they created the code to not allow @future. Since I don't have access, I won't know. Thanks for digging in, at least now I know it's possible! I came up with another solution using a trigger, but was much more inelegant than I was going for. – Nathan Williams Nov 22 '14 at 7:11

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.