Sunday, December 27, 2015

Self recurring class to create test records in Salesforse object

While working on Round Robin Incidents Assignment Routine for BMC Remedyforce we've built a tool to auto create test Incident records. This tool creates new record every one minute simulating real environment.

After minor modifications the same solution could be used for similar purposes to simulate creating records in standard or custom from SFDC UI. Below we provided example of self recurring class to create test Contact records.

The code has comments however please feel free to Contact Us if you have any questions.

APEX Class: AutoContactsCreator
/*
Version      : 1.0
Company      : WebSolo inc.
Date         : 12.2015
Description  : APEX class to automatically create new test Contact record (with some test data) each 5 minutes
History      :             
*/
global class AutoContactsCreator implements Schedulable{
 //Execute method
    global void execute(SchedulableContext SC) {
        //Code to check if test Account with name 'Test Account' exists and to create one if not.
        List acc = [SELECT id FROM Account WHERE Name = 'Test Account' limit 1];
        if(acc.size() == 0){
            Account NewAcc = new Account();
            NewAcc.Name = 'Test Account';
            insert NewAcc;
            acc.add(NewAcc);
        }
        //Code to check if test Contacts (created before with name AutoContact) exist and to identify the most recent Name (to auto increase name for new one).
        Contact con = new Contact();
        con.AccountId = acc[0].id;
        //Get next number of autocontact
            //Get last AutoContact record and parce LastName value
             List lastCon = [SELECT Name FROM Contact WHERE LastName LIKE 'AutoContact%' ORDER BY CreatedDate DESC LIMIT 1];
             //If exist, substring most recent name OR start form 1
             Integer NumOfNextAutoCon;
             if(lastCon.size() != 0){
                 String nameOfLastAutoCon = lastCon[0].Name;
                 NumOfNextAutoCon = Integer.valueOf(nameOfLastAutoCon.substringAfter('AutoContact')) + 1;
             }
             else{
                NumOfNextAutoCon = 1;
             }
        //Prepare values for First/Last Name, Phone, Email     
        con.FirstName = 'Test';
        con.LastName = 'AutoContact' + NumOfNextAutoCon;
            //Generate random number for phone from 1000000 to 9999999
            Integer uniqueVal =  Math.round(Math.random()*1000000) + 999999 ;
        con.Phone = '416' + uniqueVal;
        con.Email = 'AutoContact' + NumOfNextAutoCon + '@TestAccount.com';
        insert con;
        
        //This code section will schedule next class execution in 5 minutes from now
        datetime nextScheduleTime = system.now().addMinutes(1);
        string minute = string.valueof(nextScheduleTime.minute());
        string second = string.valueof(nextScheduleTime.second ());
        string cronvalue = second+' '+minute+' * * * ?';
        string jobName = 'AutoContactsCreator ' + nextScheduleTime.format('hh:mm');
        AutoContactsCreator p = new AutoContactsCreator();
        system.schedule(jobName, cronvalue , p);
 
        //This code section to be used to abort auto-scheduled job
        system.abortJob(sc.getTriggerId());
    }
    //Method to start shedule job form console
    public void startJob(){
        datetime nextScheduleTime = system.now().addMinutes(1);
        string minute = string.valueof(nextScheduleTime.minute());
        string second = string.valueof(nextScheduleTime.second ());
        string cronvalue = second+' '+minute+' * * * ?' ;
        string jobName = 'AutoContactsCreator ' + nextScheduleTime.format('hh:mm');   
        AutoContactsCreator p = new AutoContactsCreator();
        system.schedule(jobName, cronvalue , p);    
    }
    //Method to abort shedule job (with 'AutoContactsCreator' name) form console
    public void deleteJob(){
        CronTrigger job = [SELECT Id, CronJobDetail.Id, CronJobDetail.Name, CronJobDetail.JobType FROM CronTrigger where CronJobDetail.Name LIKE 'AutoContactsCreator%'];
        system.abortJob(job.Id);
    }
 }

APEX Class: AutoContactsCreator_TESTclass
/*
Version      : 1.0
Company      : WebSolo inc.
Date         : 12.2015
Description  : APEX test coverage class for AutoContactsCreator class
History      :             
*/
@isTest
private class AutoContactsCreator_TESTclass {
    static testMethod void myUnitTest() {
        AutoContactsCreator autoCrCon = new AutoContactsCreator();
        autoCrCon.StartJob();
        autoCrCon.deleteJob();
        system.schedule('Test', '0 0 * * * ?' , autoCrCon);
    } 
}

SFDC Developer Console commands to use with AutoContactsCreator class

To execute self recurring class
AutoContactsCreator autoCrCon = new AutoContactsCreator();
autoCrCon.StartJob();

To abort self recurring class
AutoContactsCreator autoCrCon = new AutoContactsCreator();
autoCrCon.deleteJob();
Note: You also can abort the job manually here Setup -> Jobs -> Scheduled Jobs

Helpful command to DELETE all previously created by self recurring class test records
DELETE [SELECT id FROM Contact WHERE LastName LIKE 'AutoContact%'];

Monday, June 8, 2015

Custom Salesforce Round Robin Incidents Assignment Routine for BMC Remedyforce

Salesforce Round Robin Assignment Routine for BMC Remedyforce was built for companies wanting to automatically distribute and assign incoming Incidents to the Members of different Support Teams (Groups) depending on Incident creation time and Group's Time Zone.

We have multiple Assignment Routine implementations for different businesses who use BMC Remedyforce and Salesfroce. Armed with multiple extra features Round Robin Incidents Assignment Routine for BMC Remedyforce works extremely well and deserved great references.

For instance please take a look at this reference - shared by our client who we recently helped with the RemedyForce Round Robin customization:
https://communities.bmc.com/ideas/3675#comment-62436

Please do not hesitate to Contact Us if you have any questions or wish to add this extremely useful functionality to your Salesforce with Remedyforce instance. We'll happy to reply with answers, licensing options and pricing details.


Round Robin for BMC Remedyforce
Most important Functional Features:

- "Assignment Groups" Tab (Console) to manage multiple Groups of Users involved in "Round Robin" assignment routine and monitor their current indicators such as Time Zone, Assignment Score, Status, etc.
- Capability to enable/disable particular Group(s) from round robin cycle
- Capability to assign Members to Groups based on Shifts ("Assignment Time Frame" feature)
- Capability to activate/deactivate Group's Members from round robin cycle if the Member is sick or on vacation
- Potential capability for Members to "check out/check in" from/to round robin cycle ("Self Management" feature)
- Capability to set Share for each Member in the Group - as a % of the total records received from assignment Queue during the day ("Not Equal Share" feature)
- Reporting capabilities to help manager to track Round Robin Assignment statistics

Other Details:
- 100% Native SFDC Application. Means no servers or outside services required.
- Supports following Salesforce.com Editions: Enterprise, Unlimited, Developer
- Instant processing time, no assignment delays

Solution functionality could be amended and enhanced to meet additional particular business requirements. Here are few possible scenarios:
- "Day time" Group(s) to handle the normal working day Incidents and "on call" Group for off-hours support
- Assign Incidents to Group(s) designated to particular Incident Priority/Impact or any other set of custom criteria
- Auto login/logout Members from/to Round Robin cycle based on their login and log out time in Salesforce. So that they wouldn't receive Incidents if they are not actually working now.

Screens
1) Assignment Groups ConsoleAssignment Groups Console

2) Assignment Group layout
Assignment Group layout

 3) Group Member layout
Group Member layout

Thursday, May 14, 2015

Let your Contacts update their Email preferences using Force.com Site without authentication. Configuring the public Force.com Site.

Force.com Sites is relatively known feature which allows SFDC developers to set up public-facing sites/pages hosted by Salesforce platform. Using Force.com Site you can build micro web sites with one landing page or complicated sites with multiple pages. Such sites could have complicated flows to expose/collect information from/to Salesfroce. Advanced Force.com sites could even (for instance) accept credit card payments.

In this post we will show how to built quite easy but useful Force.com site and page with a purpose to let your Salesforce Contacts update their Email preferences themself, without contacting your support team. I.e. unsubscribe by updating Email Opt Out checkbox in related Salesforce record.

Solution could be modified based on particular business requirements.
Please do not hesitate to contact us if you want to leverage this solutions in your Saleforce instance.

Here's how the solution flow works:
  1. SFDC (or mass mail application) sends marketing emails out to customers (SFDC Contacts).
  2. Email will consist of "manage my subscription" or "unsubscribe" link. The link will be unique for each Contact and consist of "encrypted" Contact's ID. Encrypting is recommended to hide real SFDC Contatc ID (15-character) for security reasons. See the link with “hash id” (in yellow) below as an example.
  3. End user clicks on that URL and opens a Force.com site page, change his Email preferences and click "Update"to commit updates to SFDC.
Unique URL in email for end customer will looks like this:
http://yourdomain.force.com/emailpreferences/profile/fd90b5cd3bda11119f79a0c8131ab2ac7266d3f54add67f276c396e74f906da4

Step 0: Create new trigger to populate HashId field on Contact object

As mentioned above we suggest to encrypt native SFDC ID to improve your Force.com site security.
This technique allows to hide and not publicly expose your SFDC Contact record's ID. Instead of this Force.com site will use Crypto generated (algorithm SHA256) compact representations of the original ID value which will be "decrypted" back by URL rewriter controller class (see below).

First please create new HashId [HashId__c, Text (255)] field on Contact object. To make the field indexed by SFDC (and improve Force.com site response time) set External ID check box to TRUE.
Then create ContactHashId trigger to populate the field with encrypted value. 

NOTE: Trigger fires on "after insert", "before update" events on Contact object. Please make sure you will "update" existing Contacts using APEX scheduled Job or DataLoader.


APEX trigger: ContactHashId
/*
Version      : 1.0
Company      : WebSolo inc.
Date         : 05.2015
Description  : trigger to populate the HashId field on Contact object (used by PublicEmailPreferencesURLRewriter class)
History      :             
*/

trigger ContactHashId on Contact (after insert, before update) {

    // Generate hash ids for all contacts that don't have them.
    List<Contact> contactsToUpdate = new List<Contact>();
    for (Contact c : Trigger.new) {
        if (c.HashId__c == null) {
            String hashId = EncodingUtil.convertToHex(Crypto.generateDigest('SHA-256', Blob.valueOf((String)c.Id)));
            if (Trigger.isInsert) {
                contactsToUpdate.add(new Contact(Id = c.Id, HashId__c = hashId));
            } else {
                c.HashId__c = hashId;
            }
        }
    }
    if (!contactsToUpdate.isEmpty()) {
        update contactsToUpdate;
    }
}

Step 1: Set up Unique Domain Name for your SFDC instance

You can set up your Force.com Site by going to Setup | Develop | Sites

If your company does not have Force.com Site yet, you will be able to register a Force.com domain name that you like (upon availability).

NOTE: Be careful when registering the domain name on your Production instance. After registration you will not be able to change it! As per Salesforce "You cannot modify your Force.com domain name after the registration process."

Step 2: Create Force.com site components (VF page, APEX controllers, template) 

Create and save following Force.com site components.

VF page (Active Site Home Page): PublicEmailPreferences
<!-- 
Version      : 1.0
Company      : WebSolo Inc.
Date         : 05.2015
Description  : VF page "PublicEmailPreferences" to serve as a page in Force.com site
History      :             
-->

<apex:page controller="PublicEmailPreferencesController" cache="false" expires="0" showHeader="false" sidebar="false">

<style>
body { font-size: 100%; }
.contactInfo { margin: 20px 0px; }
.contentPanel { float: left; margin: 5px 0px 20px 20px; }
.contactLabel { float: left; font-weight: bold; width: 10em; }
h1 { color: #cc6611; font-size: 200%; padding-top: 25px;}
input[type="checkbox"] { height: 20px; width: 20px; }
.detailPanel { float: left; margin-left: 10px; width: 16em;margin-top:10px }
.logo { margin: 0px 100px 0px 20px; width: 250px; }
.selectionLabelPanel { float: left; margin-bottom: 10px; }
.selectionLabel { font-weight: bold; }
.selectionCheckboxPanel { float: right; }
</style>

<apex:composition template="{!$Site.Template}">
<apex:define name="logo">
<!-- 
You can add the logo here as a link to static resource 
<apex:image styleClass="logo" value="{!$Resource.LOGO_RESOURCE_NAME}" />
-->
</apex:define>

<apex:define name="headline">
  <h1>Email Opt Out Preferences</h1>
</apex:define>

<apex:define name="body">

  <apex:form >
  <apex:outputPanel id="leftPanel" layout="block" rendered="{!showPrefs}" styleClass="contentPanel" >    
    <apex:outputPanel layout="block" >
      <apex:outputText value="Keep your email preferences up to date." />
    </apex:outputPanel>    
    <apex:outputPanel layout="block" styleClass="contactInfo" >
      <apex:outputPanel layout="block" styleClass="contactLabel" >
        <apex:outputText value="Email" />
      </apex:outputPanel>
      <apex:outputPanel layout="block" >
        <apex:outputText value="{!contact.Email}" />
      </apex:outputPanel>
      <apex:outputPanel layout="block" styleClass="clearBoth" />
    </apex:outputPanel>    
    <apex:outputPanel layout="block" styleClass="contactInfo" >
      <apex:outputPanel layout="block" styleClass="contactLabel" >
        <apex:outputText value="First Name" />
      </apex:outputPanel>
      <apex:outputPanel layout="block" >
        <apex:outputText value="{!contact.FirstName}" />
      </apex:outputPanel>
      <apex:outputPanel layout="block" styleClass="clearBoth" />
    </apex:outputPanel>    
    <apex:outputPanel layout="block" styleClass="contactInfo" >
      <apex:outputPanel layout="block" styleClass="contactLabel" >
        <apex:outputText value="Last Name" />
      </apex:outputPanel>
      <apex:outputPanel layout="block" >
        <apex:outputText value="{!contact.LastName}" />
      </apex:outputPanel>
      <apex:outputPanel layout="block" styleClass="clearBoth" />
    </apex:outputPanel>    
    <apex:outputPanel layout="block" styleClass="contactInfo" >
      <apex:outputPanel layout="block" styleClass="clearBoth" />
    </apex:outputPanel>    
    <apex:outputPanel layout="block" >
      <apex:outputText value="To request an update to your profile information, please kindly contact our " />
      <apex:outputLink target="_top" style="color:#cc6611" value="mailto:SUPPORT%40YOURCOMAPNYDOMAIN.com?subject=Please%20update%20my%20information">support team</apex:outputLink>
      <apex:outputText value="." />
    </apex:outputPanel>    
  </apex:outputPanel>
  
  <apex:outputPanel layout="block" styleClass="clearBoth" />
  
  <apex:outputPanel id="rightPanel" layout="block" rendered="{!showPrefs}" styleClass="contentPanel" >    
    <apex:outputPanel layout="block" >
      <apex:outputText value="Select the checkbox if you do not want to receive emaild form us." />     
    </apex:outputPanel>
    <apex:outputPanel layout="block" styleClass="detailPanel" >    
      <apex:outputPanel layout="block" styleClass="selectionPanel" >
        <apex:outputPanel layout="block" styleClass="selectionLabelPanel" >
          <apex:outputText styleClass="selectionLabel" value="Opt Out Of Emails" />
        </apex:outputPanel>
        <apex:outputPanel layout="block" styleClass="selectionCheckboxPanel" >
          <apex:inputCheckbox styleClass="selectionCheckbox" value="{!subEmailOptOut}" />
        </apex:outputPanel>
      </apex:outputPanel>        
    </apex:outputPanel>
    <apex:outputPanel layout="block" styleClass="clearBoth" />
    <apex:outputPanel id="buttonPanel" layout="block" >
      <apex:actionStatus id="saveStatus">
        <apex:facet name="start">
            <apex:outputPanel >
            Updating...&nbsp;<img src="{!$Resource.AnimatedBusy}" />
          </apex:outputPanel>
        </apex:facet>
        <apex:facet name="stop">
          <apex:commandButton action="{!updatePreferences}" rerender="messagesPanel" status="saveStatus" style="background: #cc6611; color: white; font-size: 200%; margin-top: 20px;" styleClass="updateBtn" value="Update" />
        </apex:facet>
      </apex:actionStatus>
    </apex:outputPanel>    
  </apex:outputPanel>

  <apex:outputPanel layout="block" styleClass="clearBoth" />

  <apex:outputPanel id="messagesPanel" layout="block" style="max-width: 400px;" >
    <apex:outputPanel layout="block" rendered="{!hasMessages}" style="{!messagePanelStyle}" >
      <apex:repeat value="{!messages}" var="message">
        <apex:outputText value="{!message}" />
      </apex:repeat>
    </apex:outputPanel>
  </apex:outputPanel>

  </apex:form>

</apex:define>

</apex:composition>

</apex:page>

NOTE: For $Resource.AnimatedBusy please use any animated GIF illustrate "progress"

VF page controller (for Active Site Home Page): PublicEmailPreferencesController
/*
Version        : 1.0
Company        : WebSolo Inc.
Date           : 05.2015
Description    : controller for PublicEmailPreferences VF page 
Update History :

*/ 

public without sharing class PublicEmailPreferencesController {

    public Contact contact { public get; private set; }
    public Boolean subEmailOptOut { public get; public set; }
    public String rsaEmail { public get; private set; }
    public String rsrEmail { public get; private set; }
    public List<String> messages { public get; private set; }
    public Boolean hasMessages { public get {return messages != null && !messages.isEmpty();} private set; }
    public String messagePanelStyle { public get {return hasErrorMessage ? ERROR_STYLE : INFO_STYLE;} private set; }
    public Boolean showPrefs { public get; private set; }
    
    private Boolean hasErrorMessage = false;
    
    public final static String URL_PARM_ID = 'id';
    public final static String INFO_STYLE = 'color: green; background-color: #efe; padding: 10px; margin: 10px; border: 1px solid green;';
    public final static String ERROR_STYLE = 'color: red; background-color: #fee; padding: 10px; margin: 10px; border: 1px solid red;';
    public final static String ERR_CONTACT_NOT_FOUND = 'We are unable to retrieve your information at this time.';
    public final static String ERR_PREFS_UPDATE_FAILED = 'We are unable to update your preferences at this time.';
    public final static String ERR_PREFS_UPDATE_SUCCEEDED = 'Your preferences have been updated.';
    
    public PublicEmailPreferencesController() {
        
        clearMessages();
        
        String contactId = ApexPages.currentPage().getParameters().get(URL_PARM_ID);
        System.debug('id=' + contactId);
        if (contactId != null) {
            try {
                contact = getContactForId(contactId);
                if (contact != null) {
                    subEmailOptOut = contact.HasOptedOutOfEmail;
                    showPrefs = true;
                }
            } catch (Exception e) {
                addErrorMessage(ERR_CONTACT_NOT_FOUND);
            }
        } else {
            addErrorMessage(ERR_CONTACT_NOT_FOUND);
        }
    }
    
    private void addErrorMessage(String message) {
        if (messages == null) messages = new List<String>();
        messages.add(message);
        hasErrorMessage = true;
    }
    
    private void addInfoMessage(String message) {
        if (messages == null) messages = new List<String>();
        messages.add(message);
    }
    
    private void clearMessages() {
        if (messages == null) messages = new List<String>();
        messages.clear();
        hasErrorMessage = false;
    }
    
    private Contact getContactForId(String contactId) {
        Contact contact = null;
        List<Contact> contacts = [SELECT HasOptedOutOfEmail, Email, FirstName, Id, LastName FROM Contact WHERE Id = :contactId];
        if (contacts.size() == 1) {
            contact = contacts[0];
            
        } else {
            addErrorMessage(ERR_CONTACT_NOT_FOUND);
        }
        return contact;
    }
    
    public PageReference updatePreferences() {
        try {
            clearMessages();
            contact.HasOptedOutOfEmail = subEmailOptOut;
            update contact;
            addInfoMessage(ERR_PREFS_UPDATE_SUCCEEDED);
        } catch (Exception e) {
            addErrorMessage(ERR_PREFS_UPDATE_FAILED);
        }
        return null;
    }
}


VF page (template for site VF pages): PublicSiteTemplate
<!-- 
Version      : 1.0
Company      : WebSolo Inc.
Date         : 05.2015
Description  : VF page "PublicSiteTemplate" to serve as a Force.com Site Template
History      :             
-->
<apex:page showHeader="false" sidebar="false" id="PublicSiteTemplate" cache="false" expires="0" >
    <head>
        <title>FPC</title>
        
        <style>
            #fullContainer { }
            #headerContainer { padding: 0px 10px; }
            #logoContainer { float: left; }
            #contentContainer { padding-left: 10px; }
            #headlineContainer { float: left; margin-top:10px}
            #footerContainer { padding-left: 10px; }
            #footerbar { background-color: #cc6611; line-height: 0.5em; max-width: 650px; }
            .clearBoth { clear: both; }
        </style>
    </head>
    <body>
        <div id="fullContainer">
            <div id="headerContainer">
                <div id="logoContainer">
                    <apex:insert name="logo"/>
                </div>
                <div id="headlineContainer">
                    <apex:insert name="headline"/>
                </div>
                <div class="clearBoth"></div>
            </div>
            <div id="contentContainer">
                <div id="mainContent">
                    <apex:insert name="body"/>
                </div>
                <div class="clearBoth"></div>
            </div>
            <div id="footerContainer">
                <div id="footerLinks"></div>
                <div class="clearBoth"></div>
                <div id="footerbar">&nbsp;</div>
                <div class="clearBoth"></div>
            </div>
        </div>
    </body>
</apex:page>


APEX class (URL rewriter): PublicEmailPreferencesURLRewriter
/*
Version      : 1.0
Company      : WebSolo inc.
Date         : 05.2015
Description  : controller to support URL Rewrite for Force.com VF page "PublicEmailPreferences"
History      :             
*/

global class PublicEmailPreferencesURLRewriter implements Site.UrlRewriter {
    public static final String EMAIL_PROFILE_FRIENDLY = '/profile/';
    public static final String EMAIL_PROFILE_VF_PAGE_BASE = '/PublicEmailPreferences';
    public static final String EMAIL_PROFILE_VF_PAGE = EMAIL_PROFILE_VF_PAGE_BASE + '?' + PublicEmailPreferencesController.URL_PARM_ID + '=';

    global PageReference[] generateUrlFor(PageReference[] urls) {
        
        System.debug('generateUrlFor has been invoked for ' + urls);
        PageReference[] pageRefs = new List<PageReference>();
        
        Set<Id> cIds = new Set<Id>();
        for (PageReference pRef : urls) {
            String urlStr = pRef.getUrl();
            if (urlStr.startsWith(EMAIL_PROFILE_VF_PAGE_BASE)) {
                cIds.add(urlStr.substring(EMAIL_PROFILE_VF_PAGE.length()));
            }
        }
        
        Map<Id, Contact> contactsById = new Map<Id, Contact>([SELECT Id, HashId__c FROM Contact WHERE Id IN :cIds]);
        for (PageReference pRef : urls) {
            String urlStr = pRef.getUrl();
            pageRefs.add(
                urlStr.startsWith(EMAIL_PROFILE_VF_PAGE_BASE) ?
                new PageReference(EMAIL_PROFILE_FRIENDLY + contactsById.get(urlStr.substring(EMAIL_PROFILE_VF_PAGE.length())).HashId__c) :
                pRef
            );
        }
         return pageRefs;
    }
    
    global PageReference mapRequestUrl(PageReference url) {
        
        PageReference pageRef = null;
        
        String urlStr = url.getUrl();
        System.debug('mapping url=' + urlStr);
        
        if (urlStr.startsWith(EMAIL_PROFILE_FRIENDLY)) {
            String hashId = urlStr.substring(EMAIL_PROFILE_FRIENDLY.length());
            if (hashId != '') {
                List<Contact> contacts = [SELECT Id FROM Contact WHERE HashId__c = :hashId];
                if (!contacts.isEmpty()) {
                    pageRef = new PageReference(EMAIL_PROFILE_VF_PAGE + contacts[0].Id);
                } else {
                    pageRef = new PageReference(EMAIL_PROFILE_VF_PAGE);
                }
            }
        }
        return pageRef; 
    }
}


Step 3: Configuring new Force.com Public Site

Press the "New" button to create a new Force.com Site and enter the following values – you can leave all the other fields as-is. Click Save when done.
    • Site Label – name (may contain spaces) used to identify the site in the Salesforce user interface. In this example we use EmailPreferences. See screenshot below.
    • Site Name – name (cannot contain spaces) used to identify the site in code
    • Site Contact – Salesforce user who will receive email notifications about any problems with the site.
    • Default Web Address – the part of the site URL that will appear immediately after the domain name, e.g. “domainname.force.com/defaultwebaddress”.
    • Active – makes the site available for use. Can be set manually, or the Activate and Deactivate buttons on the Site page can be used to change tise setting.
    • Active Site Home Page – name of the Visualforce page that serves as the home page of the site when it is active: PublicEmailPreferences.
    • Inactive Site Home Page – name of the Visualforce page that serves as the home page of the site when it is not active: InMaintenance (automatically generated by Salesforce).
    • Site Template – name of the template that specifies the structure of all site pages: PublicSiteTemplate.
    • URL Rewriter Class – name of the class that translates public-facing URLs to internal ones: PublicEmailPreferencesURLRewriter. This class converts the public-facing contact ids into Salesforce record ids, so it provides one level of data security.
    • Clickjack Protection Level – the recommended value is compatible with the operation of the site.
Configuring new Force.com Public Site


Step 4: Adjusting Site Profile Permissions

Please proceed up with following steps:
    • On the Site Details page, click Public Access Settings.
    • Click Object Settings select Contact object and click Edit.
    • For Email Opt Out field tick Read and Edit 
    • Click Save.
    • Return to Profile Overview page select Visualforce Page Access and click Edit.
    • On the Enabled Visualforce Page list, select following pages and click Save:
      • PublicEmailPreferences
      • PublicEmailTemplate
    • Return to Profile Overview page select Apex Class Access and click Edit.
    • On the Enabled Apex Classes list, select following classes and click Save:
      • PublicEmailPreferencesController 
      • PublicEmailPreferencesURLRewriter
    • Note that the Enabled Visualforce Pages list may (and should) automatically include default pages associated with the site.
Step 5:  Test your new Force.com Site

When all done you can open the Force.com site URL an test it in action.
See how it suppose to looks like on our screenshot.


Force.com Site

Wednesday, March 18, 2015

Pop Up VisualForce page with Auto Close and Parent Refresh features

Building custom solutions in Salesforce you often need to use pop up window with embedded VisualForce page to support required functionality. You can use "modal dialog box" solution. But generally it's more complicated and required usage of heavy external javascript libraries

Let's say you want to create a custom button in layout to call Visual force page as a popup window. In most cases following two features will make final solution much more efficient and user friendly:

Auto Close the pop up if window lost the focus or click on Cancel button.
Without such feature pop up window will require manual close and new click to the button will create another pop up window. Which is confusing the end user.

Refresh Parent window after click on Save button.
If your Visual force page in pop up should update some date in parent cage (recalculate the field, update related list, etc.) you might want this feature. Without it parent SFDC page will stay AS IS as end user will have to refresh the page manually to see updated results.

Step 1: Create a Visual force page and controller
VF page: PopUpAutoCloseAndRefresh
<!-- 
Version      : 1.0
Company      : WebSolo Inc.
Date         : 03.2015
Description  : VF page "PopUpAutoCloseAndRefresh" 
History      :             
-->
<apex:page controller="PopUpAutoCloseAndRefreshContr" sidebar="false" showHeader="false"  >
    <div id="ch" style="display: none;">{!checkParam}</div>
    <script>
    window.onload = function(){  
        window.onblur = function(){
            window.close();
        }
        if(document.getElementById('ch').innerHTML == "Ok!")
        {
            window.opener.location.href="/{!$CurrentPage.parameters.id}";
            window.top.close();
        }
    }
    </script>
    <p style="font-size: 16px; font-weight: bold; margin-left: 10px">Pop Up Auto Close And Refresh Demo</p>
    <apex:form >
        <apex:pageBlock >
            <apex:pageMessage severity="info" strength="1" summary="{!messsage}" escape="false"/> 
            <apex:commandButton value="Close" action="{!CloseAndRefresh}"/>
        </apex:pageBlock>
    </apex:form>
</apex:page>

APEX Class: PopUpAutoCloseAndRefreshContr
/*
Version      : 1.0
Company      : WebSolo inc.
Date         : 03.2015
Description  : controller for VF page "PopUpAutoCloseAndRefresh"
History      :             
*/
public class PopUpAutoCloseAndRefreshContr{
    public String messsage {get;set;}
    public String checkParam {get;set;}
    public PopUpAutoCloseAndRefreshContr(){
        checkParam = 'not Ok!';
        messsage = 'Please click outside of the pop up window for Auto Close<br/>Please click  Close  to  see how parent window will be refreshed';
    }
    public PageReference CloseAndRefresh(){
        checkParam = 'Ok!';
    return null;
    }
} 

Step 2: Create a custom button (for target object) to call the VF page
Custom Button: CloseAndRefresh
Label: CloseAndRefresh
Object Name : YOUR OBJECT
Name: CloseAndRefresh
Behavior: Execute JavaScript
Display Type: Detail Page Button
OnClick JavaScript:
var url = "/apex/PopUpAutoCloseAndRefresh?id={!Opportunity.Id}"; 
var width = "520"; 
var height = "400"; 
window.open(url, '','scrollbars=no,resizable=no,status=no,toolbar=no,menubar=no, width=' + width + ',height=' + height + ',left=' + ((window.innerWidth - width)/2) + ',top=' + ((window.innerHeight - height)/2) );

Step 3: Add created custom button to layout.

To see how the provided example works in real solution please see out post "Custom Validation For Lead Conversion"

Tuesday, February 24, 2015

Round Robin Lead Assignment Routine

There are two most popular Assignment Routines to assign new Leads to Sales Representatives: Lead Bucket and Round Robin. Both have it's PROS and CONS but in case if you wants to automatically distribute and assign incoming Leads to the members of your Sales Team you may want to use "round-robin" allocation process.

Pure "round robin" technique assumes that you want to divvy out all new Leads equally among the Reps in the Sales Team. Let's say you have three Sales Reps. In this case Lead_1 will go to Rep_1, Lead_2 will go to Rep_2, Lead_3 will go to Rep_3, Lead_4 will go to Rep_1 (!),  Lead_5 will go to Rep_2 and so on.

One of our customers gets about 300 Leads per day submitted to Salesfroce from Web site through Web-to-Lead form. Sales Team has about ten Sales Reps with different level of experience (including Interns, Senior Reps and Sales Reps with less skill).

BUSINESS REQUIREMENTS: Create Assignment Routine to automatically assign Leads (based on "round-robin" logic) with ability to easy activate/deactivate Reps (from round robin cycle if the Rep is sick or on vacation) and set it's Share - as a % of the total Leads received from Leads Queue during the day ("not equal share" feature).

Let's review all possible "round robin" solutions which could meet the requirement (fully or partially) along with implementation details and all its PROS and CONS.

Update 06/08/2015: Please also see Custom Salesforce Round Robin Incidents Assignment Routine for BMC Remedyforce

#1 Auto Number field and Lead Assignment Rules

We want to present this option as a "native" Salesforce solution which does not required any APEX coding. Here is a plan for Sales Team with 3 members:
  1. On the Lead object create new "Lead RR Number" field (Data Type: Auto Number, Display Format: {0}, Starting Number:1
    As a result every new Lead will get an unique number with 1 as an increment.
  2. On the Lead object create new "Lead RR ID" field  (Data Type: Formula, Formula Value: MOD(VALUE({!Lead_RR_Number__c}),3) +1
    Formula will return number from 1 to 3 depend on value in "Lead RR Number" field.
  3. Create 3 new Lead Assignment Rules with criteria for the rule entry as following:
    Lead: Lead RR ID equals 1 / User to Assign the Lead: Rep_1; Lead: Lead RR ID equals 2 / User to Assign the Lead: Rep_2; Lead: Lead RR ID equals 3 / User to Assign the Lead: Rep_3
    Activate new Rules and check how the solution works in action. Every new created user suppose to be assigned to Sales Rep as per expected "round robin" logic.
PROS: Use only SFDC native "out of box" tools to build
CONS: Bulky, quite complicated and time consuming to manage. Every time when you want to add/delete/activate/deactivate the Rep in the Sale Team you have to edit the formula and Assignment rules. Easy to make a mistake and compromise the assignment flow. Can't support "not equal share" requirements.

#2 Custom Force.com solution with new object and APEX trigger

After careful consideration we proposed and built following Force.com solution which meets all requirement.

SOLUTIONS DETAILS:
1) New Custom object "Sales Representatives" to keep info about Users involved in "round robin" assignment flow.
Consist of following fields:
 - User (lookup to User)
 - Status (Active/Inactive)
 - Split Weight (number from 1 to 100)
 - Split % (dynamically calculated based on number of Active users and their "Split Weight". For instance if you have 3 active Users and their Split Weight set to 20/30/30 their Split % will be calculated as 25%/37.5%/37.5%)
 - Next Assign To (check box to dynamically show who will get next Lead from Queue)
 - Today's Score (number of assigned TODAY Leads)

In SFDC interface Administrator can easy amend the Weight and Status for each user. Also he has an ability to add/delete records (Sales Reps) in the object.

Round Robin Lead Assignment Routine













2) Trigger SplitCalculation to auto recalculate Split % for each active user if Split Weight or Status of any User changed. Split % for each inactive user to be set to 0. Split % will be used in assignment logic.

3) Overnight job (at 0:00) to reset all users Today's Score to 0.

4) Trigger  LeadAssignment (before insert) to fire for each created Lead (some filters could be applied). Trigger (and dedicated Helper class) does following:
 - assign  the  Lead  to  the  User  who is next in the line (based on Next Assign To check box)
 - update assigned User's Today's Score based on number of assigned today Leads (+1)
 - identify Next Assign To User as per following logic (assigning  Lead to the users on a round robin basis. Count Users score and skip User if he's got as much Leads to hit his User's Split %).

PROS: Meets ALL Business Requirements
CONS: None. Required new object and some APEX coding.

Please do not hesitate to contact us if you want to leverage this solutions in your Saleforce instance.

#3 App by Salesforce Labs: Round Robin Record Assignment

Advanced Salesfroce users with Administrator experience might want to install and amend the Round Robin Record Assignment app from Force Labs. This free solution was built to manage Cases assignment. But APEX code is open to modify. So with some experience and knowledge you can amend the app to work with Leads object. 

PROS: Free app from Force Labs. Has some extra features.
CONS: Does not support "not equal share" requirements. Has some limitations and average 3.7 of 5 stars reviews. 

Friday, January 9, 2015

How to instantly see potential duplicates on the Lead page

Over the time company's sales representatives could create Duplicated Leads in Salesforce. Often it creates awkwardness and challenges. For instance when different reps call to the same individual. As a result the company could lose the sale opportunity right from the start.

Salesforce has native “Find Duplicates” button located on the Lead layout but reps not always use it BEFORE the actual call or email to potential buyer. It also time consuming to check out every Lead.

Recently we received following request from one of our Customers: "Is it possible to see duplicates on the lead’s front page (ie without clicking find duplicates)? Sort of like how I can see open activities and activity history without scrolling down. This would save me a click every time on Find Duplicates button to check the same."

So Customer wants from one glance to see if the current Lead has potential duplicates  - without extra clicks. And only click to "Find Duplicates" button for search and merge duplicated Lead if needed.

You can find details of solution we've built below.

Solution to instantly show list of duplicated Leads on the Lead layout.

1) VF page (SeeDuplicatesLeads) to be embedded to Lead Layout. VF page shows list of potential duplicates of the current Lead. List consists of following columns: 
Name | Last Name | Company | Email | Phone | Lead Owner 

2) APEX controller uses similar to native "Find Duplicates" button's matching logic to identify Lead's duplicates (the same Lead Name OR the same Company OR the same Email OR the same Email Domain OR the same Phone).

See code samples below.

Extra note: Solution uses the Dynamic SOQL approach to avoid issues when one or more of the filed(s) used in SOQL have NULL value.

VF page: SeeDuplicatesLeads
<!-- 
Version      : 1.0
Company      : WebSolo Inc.
Date         : 01.2015
Description  : VF page "SeeDuplicatesLeads" to to instantly show list of duplicated Leads on the Lead layout
History      :             
-->
<apex:page standardController="Lead" extensions="SeeDuplicatesLeads">
     <apex:pageBlock >
        <apex:pageBlockTable value="{!LeadList}" var="tl">
            <apex:column headerValue="Name">
                <a href="/{!tl.id}" target="_top" id="{!tl.id}">{!tl.Name}</a>
            </apex:column>            
            <apex:column headerValue="Email" value="{!tl.Email}"/>
            <apex:column headerValue="Company" value="{!tl.Company}"/>
            <apex:column headerValue="Phone" value="{!tl.Phone}"/>
            <apex:column headerValue="Lead Owner" value="{!tl.Owner.Name}"/>
        </apex:pageBlockTable>
    </apex:pageBlock>   
</apex:page>

APEX Class: SeeDuplicatesLeads
/*
Version      : 1.0
Company      : WebSolo inc.
Date         : 01.2015
Description  : controller for VF page "SeeDuplicatesLeads"
History      :             
*/
public class SeeDuplicatesLeads 
{
    public id leadId {get; set;}
    public list<Lead> LeadList {get; set;}
    public SeeDuplicatesLeads(ApexPages.StandardController controller) 
    {
        leadId = ((Lead) controller.getRecord()).id;
        LeadList = new list<Lead>();
        Lead leadobj = [select id, Name, Email, Company, Phone, OwnerId, Owner.Name from Lead where id=: leadId limit 1];
        String sqlStart = 'select id, Name, Email, Company, Phone, OwnerId, Owner.Name from Lead where ';
        String sqlEnd = ' and id !=\'' + leadobj.id + '\'';
        String sqlwhere = '';
        if(leadobj.Email == null)
        {
            if(leadobj.Company == null)
            {
                if(leadobj.Phone == null)
                {
                     sqlwhere = 'Name =\'' + leadobj.Name + '\'';
                }
                else
                {
                     sqlwhere = '(Name =\'' + leadobj.Name + '\' or Phone =\'' + leadobj.Phone + '\')';
                }
            }   
            else
            {
                if(leadobj.Phone == null)
                {
                    sqlwhere = '(Name = \'' + leadobj.Name + '\' or Company =\'' +  leadobj.Company + '\')';
                }
                else
                {   
                    sqlwhere = '(Name = \'' + leadobj.Name + '\' or Company = \'' + leadobj.Company + '\' or Phone = \'' + leadobj.Phone + '\')';   
                }
            }    
        }  
        else
        {
            list<String> EmailDomain = leadobj.Email.split('@');
            list<Lead> LeadListNew = new list<Lead>();          
            if(leadobj.Company == null)
            {
                if(leadobj.Phone == null)
                {
                     sqlwhere = '(Name =\'' + leadobj.Name + '\' or Email LIKE \'' + EmailDomain[1] + '\')';
                }
                else
                {
                     sqlwhere = '(Name =\'' + leadobj.Name + '\' or Phone =\'' + leadobj.Phone + '\' or Email LIKE \'' + EmailDomain[1] + '\')';
                }
            }   
            else
            {
                if(leadobj.Phone == null)
                {
                    sqlwhere = '(Name = \'' + leadobj.Name + '\' or Company =\'' +  leadobj.Company + '\' or Email LIKE \'' + EmailDomain[1] + '\')';
                }
                else
                {   
                    sqlwhere = '(Name = \'' + leadobj.Name + '\' or Company = \'' + leadobj.Company + '\' or Phone = \'' + leadobj.Phone + '\' or Email LIKE \'' + EmailDomain[1] + '\')';   
                }
            }           
        } 
        LeadList = Database.query(sqlStart + sqlwhere + sqlEnd);                                                                                                                                                                                                                                                                                                                    
        if(LeadList.size() > 100)
        {
            list<Lead> goodLeadlist = new list<Lead>();
            Integer Igood = 100;
            for(Integer I = 0; I < Igood; I++)
            {
                goodLeadlist.add(LeadList[I]);
            }
            LeadList.clear();
            LeadList.addall(goodLeadlist);          
        }
    }
}