Apex SF Standard 

The only coded way to get Opportunity Stages by Record Type (add Probabilities to taste)

We learned last week that Metadata API can be pretty cool, huh? Sometimes it’s even the only way to retrieve what you expect to be covered by standard Apex functions, but unfortunately isn’t.

Let’s say you’d need the mapping from Opportunity Stage to the associated Probability per Record Type / Sales Process (in my case the requirement was to decouple Opportunity Probability from Stage, so you can manually enter a Probability – as long as the Probability occurs in your Sales Process, there is no need for it to match the Stage, but never mind). You find these closely related pieces of information in various places in the UI, so wouldn’t it make sense to provide something that basic as some kind of DescribeResult?

Well, Salesforce didn’t consider this necessary, but fortunately introduced Custom Metadata Types, which we can utilize by creating a new Custom Metadata Type, emulating the missing metadata and making the requested information available to be retrieved on the fly, in a resource-saving manner, in Apex code.

  • SalesProcessMapping__mdt

with the fields

  • RecordTypeId__c (Text, 18)
  • StageName__c (Text, 255)
  • optional: Probability__c (Integer, 3/0)

Now I could just tell you to populate the metadata manually and keep it up to date once stages or probabilities or record types change, we could finish here and you’d rightfully call me an awful blogger. Or we do it the proper, fully automated way, with Metadata API saving the day once more.

We’re relying once more on the service of MetadataService, which has been woven into the architectural design of my choice – a schedulable job which replenishes the Custom Metadata records by deleting, then rebuilding all records on execution. MetadataService helps in numerous ways:

  • Deleting existing SalesProcessMapping__mdt custom metadata in the execute() method
  • Retrieving the name of the associated Business Process of every Opportunity Record Type
  • Retrieving all available picklist values of every Business Process
  • Re-deploying the new SalesProcessMapping__mdt custom metadata records

Some pitfalls and caveats as follows:

  • The OpportunityStage object reveals the stage-to-probability mapping and should be used if recording Probability values
  • Picklist Values from the Metadata API are URL encoded, and need to be decoded before matching against values from Schema methods or OpportunityStage object query results, which are not encoded.

Enough said, let’s proceed to the golden lines:

public without sharing class DailyPopulateOppSalesProcessMetadata implements Database.Batchable<sObject>, Database.Stateful, Database.AllowsCallouts {
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator([SELECT Id, DeveloperName FROM SalesProcessMapping__mdt]);
    }
    
    // In order to fully reset metadata at every execution, we use the execute method to delete all existing metadata and the finish method to reconstruct them.
    public void execute(Database.BatchableContext bc, List<SalesProcessMapping__mdt> scope){
        MetadataService.MetadataPort service = new  MetadataService.MetadataPort();
        service.SessionHeader = new MetadataService.SessionHeader_element();
        service.SessionHeader.sessionId = UserInfo.getSessionId();
        service.timeout_x=120000;
        List<String> metadataToDelete = new List<String>();
        for(SalesProcessMapping__mdt mdt : scope){
            metadataToDelete.add('SalesProcessMapping__mdt.' + mdt.DeveloperName);
        }
        if(metadataToDelete.size() > 0){
            service.deleteMetadata('CustomMetadata', metadataToDelete);
        }       
    } 

    public static MetadataService.MetadataPort createService(){
        MetadataService.MetadataPort service = new MetadataService.MetadataPort();
        service.SessionHeader = new MetadataService.SessionHeader_element();
        service.SessionHeader.sessionId = UserInfo.getSessionId();
        service.timeout_x=120000;
        return service;
    }
    
    public void finish(Database.BatchableContext bc){ 
        List<SalesProcessMapping__mdt> remainingMetadata = [SELECT Id, DeveloperName FROM SalesProcessMapping__mdt];
        if(remainingMetadata.isEmpty()){
            // Repopulate SalesProcessMapping__mdt records if there are none left
            Map<String, Decimal> stageToProbMap = new Map<String, Decimal>();
            for(OpportunityStage stg : [SELECT DefaultProbability, ApiName FROM OpportunityStage WHERE IsActive = TRUE]){
                stageToProbMap.put(stg.ApiName, stg.DefaultProbability);
            }
            Metadata.DeployContainer mdContainer = new Metadata.DeployContainer();
            String objName = 'Opportunity';
            Map<String, Schema.RecordTypeInfo> rtMapByString = Schema.SObjectType.Opportunity.getRecordTypeInfosByDeveloperName();
            Pattern nonAlphanumeric = Pattern.compile('[^a-zA-Z0-9]');
            for(String recordTypeName : rtMapByString.keySet()){
                Id recordTypeId = rtMapByString.get(recordTypeName).getRecordTypeId();
                MetadataService.MetadataPort service = createService();
                // Read Business Process from RT
                MetadataService.RecordType recordType = (MetadataService.RecordType) service.readMetadata
                    ('RecordType',new String[] {objName +'.'+ recordTypeName}).getRecords()[0];
                if(recordType != null && recordType.businessProcess != null){
                    // Retrieve Business Process
                    MetadataService.BusinessProcess businessProcess = (MetadataService.BusinessProcess) service.readMetadata
                        ('BusinessProcess',new String[] {objName +'.'+ recordType.businessProcess}).getRecords()[0];
                    if(businessProcess != null && businessProcess.values != null){
                        for(MetadataService.PicklistValue pv : businessProcess.values){
                            String decodedFullName = EncodingUtil.urlDecode(pv.fullname, 'UTF-8');
                            if(stageToProbMap.containsKey(decodedFullName)){
                                String strippedName = nonAlphanumeric.matcher(decodedFullName).replaceAll('');
                                String baseString = recordTypeName + '_' + strippedName;
                                if(baseString.length() > 40){
                                    baseString = baseString.substring(0,40);
                                }
                                // Create new custom metadata records
                                Metadata.CustomMetadata metadataRec =  new Metadata.CustomMetadata();
                                metadataRec.fullName = 'SalesProcessMapping__mdt.' + baseString;
                                metadataRec.label = baseString;
                                Metadata.CustomMetadataValue rtField = new Metadata.CustomMetadataValue();
                                rtField.field = 'RecordTypeId__c';
                                rtField.value = String.valueOf(recordTypeId);
                                metadataRec.values.add(rtField);
                                Metadata.CustomMetadataValue stageField = new Metadata.CustomMetadataValue();
                                stageField.field = 'StageName__c';
                                stageField.value = decodedFullName;
                                metadataRec.values.add(stageField);
                                // Apply Probability Field Value if using
                                Metadata.CustomMetadataValue probField = new Metadata.CustomMetadataValue();
                                probField.field = 'Probability__c';
                                probField.value = stageToProbMap.get(decodedFullName);
                                metadataRec.values.add(probField);
                                mdContainer.addMetadata(metadataRec);
                            }
                        }
                    }
                }
            }
            if(!Test.isRunningTest() && !mdContainer.getMetadata().isEmpty()){
                // Create new custom metadata records
                Metadata.Operations.enqueueDeployment(mdContainer, null);
            }    
        } else {
            if(!Test.isRunningTest()){
                // Restart the job if there are still SalesProcessMapping__mdt records
                Database.executeBatch(new DailyPopulateOppSalesProcessMetadata(), 1);
            }
        }
    }  
}

Leave a Reply

Your email address will not be published. Required fields are marked *