Create Triggers for Advanced Account Forecasting Calculations

If you’re using custom advanced account forecast fact objects for your forecasts, you can create triggers to calculate forecasts for your accounts instead of using the CalculateAdvancedForecast invocable action in the flow. A trigger is Apex code that executes before or after specific data manipulation language (DML) events occur, such as before object records are inserted into the database, or after records have been deleted.

Required Editions

Available in: Lightning Experience
Available in: Enterprise, Unlimited, and Developer Editions
User Permissions Needed
To create triggers: Customize Application
  1. From Setup, open Object Manager.
  2. Click the custom advanced account forecast fact object.
  3. Click Triggers, then click New.
  4. Define a trigger with your forecast calculation formula and then save your work.

    You trigger must include these three XML files.

    • CustomForecastFactFormulaEvaluator.cls
    • CustomForecastFactUtil.cls
    • CustomForecastFactTrigger.trigger

    If you're adding them, you must create them in this order. If you already have these files on your system and use the Metadata API to deploy them, the deployment takes care of the order.

    For example, a trigger for a CustomForecastFact__c object that calculates the forecast quantity and forecast revenue, and inserts the values in the custom fact record has these three XML files.

    public class CustomForecastFactFormulaEvaluator {
        
        Map<String, String> calculationFrequencyToPeriodTypeMap = new Map<String, String> {
            'Yearly' => 'Year', 'Quarterly' => 'Quarter', 'Monthly' => 'Month', 'Weekly' => 'Week'
        };
        
        static Map<Id, AdvAcctForecastSetPartner> partnerMap;
        static Map<Id, AdvAccountForecastSet> forecastSetMap;
        static Map<Id, AdvAcctForecastPeriodGroup> periodGroupMap;
        static Map<String, List<Period>> periodsMap;
        static Map<String, Period> currentPeriodMap;
        static Map<String, Period> pastPeriodMap;
        static Map<String, Period> futurePeriodMap;
        static Map<Id, Map<Integer, AdvAccountForecastFormula>> quantityFormulaMap;
        static Map<Id, Map<Integer, AdvAccountForecastFormula>> revenueFormulaMap;
        
    	AdvAcctForecastSetPartner partner;
        AdvAccountForecastSet forecastSet;
        AdvAcctForecastPeriodGroup periodGroup;
        Map<Integer, AdvAccountForecastFormula> quantityFormulaByPeriod;
        Map<Integer, AdvAccountForecastFormula> revenueFormulaByPeriod;
        Date forecastStartDate;
        List<Period> periods;
        List<Date> periodStartDates;
        static Map<Id, CustomForecastFactFormulaEvaluator> partnerCache = new Map<Id, CustomForecastFactFormulaEvaluator>();
            
        public static void initialize(Set<Id> partnerIds) {
            partnerMap = loadForecastSetPartners(partnerIds);
            forecastSetMap = loadForecastSets(partnerMap.values());
            periodGroupMap = loadForecastPeriodGroups(forecastSetMap.values());
            periodsMap = loadPeriods();
            pastPeriodMap = loadPastPeriodMap();
            currentPeriodMap = loadCurrentPeriodMap();
            futurePeriodMap = loadFuturePeriodMap();
            loadForecastFormulas(forecastSetMap.keySet());
        }
        
        private CustomForecastFactFormulaEvaluator(Id forecastSetPartnerId) {
            partner = loadForecastSetPartner(forecastSetPartnerId);
            forecastSet = loadForecastSet(partner.AdvAccountForecastSetId);
            periodGroup = loadForecastPeriodGroup(forecastSet.ForecastPeriodGroupId);
            loadForecastFormulas(forecastSet.Id);
            periods = loadPeriods(resolvePeriodTypeFromCalculationFrequency(forecastSet.CalculationFrequency));
            periodStartDates = extractPeriodStartDates(periods);
            forecastStartDate = getForecastStartDate(resolvePeriodTypeFromCalculationFrequency(forecastSet.CalculationFrequency), periodGroup.StartPeriod);
        }
        
        public static CustomForecastFactFormulaEvaluator getInstance(Id forecastSetPartnerId) {
            if(!partnerCache.containsKey(forecastSetPartnerId)) {
                partnerCache.put(forecastSetPartnerId, new CustomForecastFactFormulaEvaluator(forecastSetPartnerId));
            }
            return partnerCache.get(forecastSetPartnerId);
        }
        
        private AdvAcctForecastSetPartner loadForecastSetPartner(Id forecastSetPartnerId) {
            return partnerMap.get(forecastSetPartnerId);
        }
        
        private static Map<Id, AdvAcctForecastSetPartner> loadForecastSetPartners(Set<Id> forecastSetPartnerIds) {
            return new Map<Id, AdvAcctForecastSetPartner>([SELECT Id, AdvAccountForecastSetId FROM AdvAcctForecastSetPartner WHERE Id IN : forecastSetPartnerIds]);
        }
        
        private AdvAccountForecastSet loadForecastSet(Id forecastSetId) {
            return forecastSetMap.get(forecastSetId);
        }
        
        private static Map<Id, AdvAccountForecastSet> loadForecastSets(List<AdvAcctForecastSetPartner> partners) {
            Set<Id> forecastSetIds = new Set<Id>();
            for (AdvAcctForecastSetPartner partner : partners) {
                forecastSetIds.add(partner.AdvAccountForecastSetId);
            }
            return new Map<Id, AdvAccountForecastSet>([SELECT Id, ForecastPeriodGroupId, CalculationFrequency FROM AdvAccountForecastSet WHERE Id IN :forecastSetIds]);
        }
        
        private AdvAcctForecastPeriodGroup loadForecastPeriodGroup(Id forecastPeriodGroupId) {
            return periodGroupMap.get(forecastPeriodGroupId);
        }
        
        private static Map<Id, AdvAcctForecastPeriodGroup> loadForecastPeriodGroups(List<AdvAccountForecastSet> forecastSets) {
            Set<Id> forecastPeriodGroupIds = new Set<Id>();
            for (AdvAccountForecastSet forecastSet : forecastSets) {
                forecastPeriodGroupIds.add(forecastSet.ForecastPeriodGroupId);
            }
            return new Map<Id, AdvAcctForecastPeriodGroup>([SELECT Id, StartPeriod FROM AdvAcctForecastPeriodGroup WHERE Id IN :forecastPeriodGroupIds]);
        }
        
        private void loadForecastFormulas(Id forecastSetId) {
            quantityFormulaByPeriod = quantityFormulaMap.get(forecastSetId);
            revenueFormulaByPeriod = revenueFormulaMap.get(forecastSetId);
        }
        
        private static void loadForecastFormulas(Set<Id> forecastSetIds) {
            List<AdvAccountForecastFormula> formulaList = [SELECT Id, AdvAccountForecastSetId, FormulaType, StartPeriod, EndPeriod, FormulaExpression FROM AdvAccountForecastFormula WHERE AdvAccountForecastSetId IN :forecastSetIds];
            
            quantityFormulaMap = new Map<Id, Map<Integer, AdvAccountForecastFormula>>();
            revenueFormulaMap = new Map<Id, Map<Integer, AdvAccountForecastFormula>>();
            
            for(AdvAccountForecastFormula formula : formulaList) {
                Id forecastSetId = formula.AdvAccountForecastSetId;
                
                if(!quantityFormulaMap.containsKey(forecastSetId)) quantityFormulaMap.put(forecastSetId, new Map<Integer, AdvAccountForecastFormula>());
                if(!revenueFormulaMap.containsKey(forecastSetId)) revenueFormulaMap.put(forecastSetId, new Map<Integer, AdvAccountForecastFormula>());
                
                switch on formula.FormulaType {
                    when 'QUANTITY' {
                        for (Integer i = formula.StartPeriod; i <= formula.EndPeriod; i++) {
                            quantityFormulaMap.get(forecastSetId).put(i, formula);
                        }
                    }
                    when 'REVENUE' {
                        for (Integer i = formula.StartPeriod; i <= formula.EndPeriod; i++) {
                            revenueFormulaMap.get(forecastSetId).put(i, formula);
                        }
                    }
                }
            }
        }
        
        private List<Period> loadPeriods(String periodType) {
            return periodsMap.get(periodType);
        }
        
        private static Map<String, List<Period>> loadPeriods() {
            Map<String, List<Period>> periodsMap = new Map<String, List<Period>>();
            List<Period> periods = [SELECT Id, Type, StartDate FROM Period ORDER BY Type, StartDate];
            for (Period period : periods) {
                if(!periodsMap.containsKey(period.Type)) {
                    periodsMap.put(period.Type, new List<Period>{period});
                } else {
                    periodsMap.get(period.Type).add(period);
                }
            }
            return periodsMap;
        }
        
        private List<Date> extractPeriodStartDates(List<Period> periods) {
            List<Date> startDateList = new List<Date>();
            for(Period period : periods) {
                startDateList.add(period.StartDate);
            }
            return startDateList;
        }
        
        private Date getForecastStartDate(String periodType, Integer startPeriod) {
            Period forecastStartPeriod;
            
            if(startPeriod == 0) {
            	forecastStartPeriod = getCurrentPeriod(periodType);   
            } else {
                forecastStartPeriod = getPeriodByOffset(periodType, startPeriod);
            }
            
            return forecastStartPeriod.StartDate;
        }
        
        private Period getCurrentPeriod(String periodType) {
            return currentPeriodMap.get(periodType);
        }
        
        private static Map<String, Period> loadPastPeriodMap() {
            Map<String, Period> periodMap = new Map<String, Period>();
            Date today = Date.today();
            List<Period> periods = [SELECT Id, Type, StartDate, EndDate FROM Period WHERE StartDate <= :today AND EndDate <= :today LIMIT 1];
            for(Period period : periods) {
                periodMap.put(period.Type, period);
            }
            return periodMap;
        }
        
        private static Map<String, Period> loadCurrentPeriodMap() {
            Map<String, Period> periodMap = new Map<String, Period>();
            Date today = Date.today();
            List<Period> periods = [SELECT Id, Type, StartDate, EndDate FROM Period WHERE StartDate >= :today AND EndDate <= :today LIMIT 1];
            for(Period period : periods) {
                periodMap.put(period.Type, period);
            }
            return periodMap;
        }
        
        private static Map<String, Period> loadFuturePeriodMap() {
            Map<String, Period> periodMap = new Map<String, Period>();
            Date today = Date.today();
            List<Period> periods = [SELECT Id, Type, StartDate, EndDate FROM Period WHERE EndDate >= :today AND EndDate <= :today LIMIT 1];
            for(Period period : periods) {
                periodMap.put(period.Type, period);
            }
            return periodMap;
        }
        
        private Period getPeriodByOffset(String periodType, Integer startPeriod) {
            Date today = Date.today();
            if (startPeriod < 0) {
                return pastPeriodMap.get(periodType);
            } else if (startPeriod > 0) {
                return futurePeriodMap.get(periodType);
            } else {
                return getCurrentPeriod(periodType);
            }
        }
        
        public void eval(List<CustomForecastFact__c> forecastFacts) {
            List<FactWrapper> factWrapperList = createFactRecordPeriodStartDatePair(forecastFacts);
            for (FactWrapper wrapper : factWrapperList) {
                CustomForecastFact__c fact = wrapper.fact;
                Date factStartDate = wrapper.startDate;
                
                Integer periodNumber = getPeriodNumber(forecastStartDate, factStartDate);
                
                // Here is the place where we can apply formula based on various conditions.
                // What we see below is just an example.
                Double dummyFactor = 1;
                fact.ForecastedQuantity__c = dummyFactor * (nvl(fact.OpportunityQuantity__c) + nvl(fact.OrderQuantity__c) + nvl(fact.LastYearOrderQuantity__c) + nvl(fact.SalesAgreementQuantity__c));
                fact.ForecastedRevenue__c = dummyFactor * (nvl(fact.OpportunityRevenue__c) + nvl(fact.OrderRevenue__c) + nvl(fact.LastYearOrderRevenue__c) + nvl(fact.SalesAgreementRevenue__c));
            }
        }
        
        private Double nvl(Decimal input) {
            if (input == null ) return 0;
            return nvl(input.doubleValue(), 0);
        }
        
        private Double nvl(Double input) {
            return nvl(input, 0);
        }
        
        private Double nvl(Double input, Double defaultVal) {
            if (input == null) return defaultVal;
            return input;
        }
        
        private Integer getPeriodNumber(Date forecastStartDate, Date forecastFactStartDate) {
            if (forecastFactStartDate < forecastStartDate) {
                // For Past Period we will always use the formula defined for period 1.
                return 1;
            }
            Integer forecastStartDatePeriodNumber = periodStartDates.indexOf(forecastStartDate);
            Integer forecactFactStartDatePeriodNumber = periodStartDates.indexOf(forecastFactStartDate);
            return forecactFactStartDatePeriodNumber - forecastStartDatePeriodNumber + 1;
        }
        
        private String resolvePeriodTypeFromCalculationFrequency(String calculationFrequency) {
            return calculationFrequencyToPeriodTypeMap.get(calculationFrequency);
        }
        
        private List<FactWrapper> createFactRecordPeriodStartDatePair(List<CustomForecastFact__c> forecastFacts) {
            List<FactWrapper> wrapperList = new List<FactWrapper>();
            
            Map<Id, Period> periodMapById = new Map<Id, Period>(periods);
            for(CustomForecastFact__c fact : forecastFacts) {
                wrapperList.add(new FactWrapper(fact, periodMapById.get(fact.Period__c).StartDate));
            }
            
            return wrapperList;
        }
        
        private class FactWrapper {
            CustomForecastFact__c fact;
            Date startDate;
            
            FactWrapper(CustomForecastFact__c fact, Date startDate) {
                this.fact = fact;
                this.startDate = startDate;
            }
        }
    }
    public class CustomForecastFactUtil {
    	public static void calculateForecast(Set<Id> forecastFactIds) {
            List<CustomForecastFact__c> forecastFacts = loadForecastFacts(forecastFactIds);
            
            // Map of Facts by PartnerId
            Map<Id, List<CustomForecastFact__c>> factMapByPartnerId = new Map<Id, List<CustomForecastFact__c>>();
            for(CustomForecastFact__c fact : forecastFacts) {
                Id partnerId = fact.AdvAcctForecastSetPartner__c;
                if (!factMapByPartnerId.containsKey(partnerId)) {
                    factMapByPartnerId.put(partnerId, new List<CustomForecastFact__c> {fact});
                } else {
                    factMapByPartnerId.get(partnerId).add(fact);
                }
            }
    ​
            CustomForecastFactFormulaEvaluator.initialize(factMapByPartnerId.keySet());        
            for(Id partnerId : factMapByPartnerId.keySet()) {
                CustomForecastFactFormulaEvaluator formulaEvaluator = CustomForecastFactFormulaEvaluator.getInstance(partnerId);
                formulaEvaluator.eval(factMapByPartnerId.get(partnerId));
            }
            
            update forecastFacts;
        }
        
        private static List<CustomForecastFact__c> loadForecastFacts(Set<Id> forecastFactIds) {
            return [
                SELECT Id, AdvAcctForecastSetPartner__c, OrderQuantity__c, OrderRevenue__c, OpportunityQuantity__c, OpportunityRevenue__c
                  , SalesAgreementQuantity__c, SalesAgreementRevenue__c, LastYearOrderQuantity__c, LastYearOrderRevenue__c
                  , ForecastedQuantity__c, ForecastedRevenue__c, Period__c
                FROM CustomForecastFact__c
                WHERE Id IN :forecastFactIds
            ];
        }
    }
    trigger CustomForecastFactTrigger on CustomForecastFact__c (after insert) {
        CustomForecastFactUtil.calculateForecast(Trigger.newMap.keySet());
    }