Skip to main content
Full Circle Insights

Building a Campaign Attribution Plugin Part 2 – Assigning Attribution

During campaign attribution processing, it is the responsibility of your plugin to assign revenue to individual responses or campaigns. You'll be assigning three values:

  • Actual revenue – Corresponds to revenue from opportunities considered closed/won.
  • Pipeline revenue – Corresponds to revenue from open opportunities.
  • Lost revenue – Corresponds to revenue from opportunities considered closed/lost.

You'll also optionally be defining the first and last touch responses for each opportunity, and optionally an additional lookup field for models that break down attribution to further details (such as by product).
There are two approaches for determining which CampaignMember objects are related to a given opportunity. One is generally based on the opportunity, and the other is generally based on the account that the opportunity belongs to. You may use either, but not both, of these approaches in your plugin. 
If you wish to use the opportunity based approach, your plugin should return true to the GetSupportsOpInfluence method call, and the framework will call the OpportunityDetermineAttribution method. If you wish to use the account based approach, your plugin should return true to the GetSupportsAccountInfluence method and the framework will call the AccountDetermineAttribution method.

The OpportunityRevenue Object

The FCI_CampaignInfluenceAPI.OpportunityRevenue object is used to return detailed information on revenue attribution to the campaign attribution application. The object is defined as follows:

 

global virtual class OpportunityRevenue
{
    global ID OpportunityID;
    global Decimal InfluenceAmount;
    global Boolean IsFirstTouchResponse;
    global Boolean IsLastTouchResponse;
    global ID ObjectLookupID;
         
    global OpportunityRevenue(ID opid, Decimal amount)
    {
        OpportunityID = opid;
        InfluenceAmount = amount;
        IsFirstTouchResponse = false;
        IsLastTouchResponse = false;
        ObjectLookupID = null;
    }
    global OpportunityRevenue(ID opid, Decimal amount, ID lookupId)
    {
        OpportunityID = opid;
        InfluenceAmount = amount;
        IsFirstTouchResponse = false;
        IsLastTouchResponse = false;
        ObjectLookupID = lookupId;
    }
}


There are two constructors, one with an extra parameter for defining the ObjectLookupId.

  • OpportunityID is the ID of an opportunity that contributes to revenue on a CampaignMember.
  • InfluenceAmount is the amount of revenue attributed to the opportunity.
  • IsFirstTouchResponse should be set to true if this model has determined that the response for this OpportunityRevenue object is the first touch response on the opportunity.
  • IsLastTouchResponse should be set to true if this model has determined that the response for this OpportunityRevenue object is the last touch response on the opportunity.
  • ObjectLookupID is the ID of a secondary object when categorizing attribution within opportunities. For example, a product ID when a model is subdividing attribution attribution for a given opportunity among multiple products.

You can automatically populate lookup fields that you create on the Campaign Attribution detail object in order to support detailed reporting based on the ObjectLookupIDs specified in the OpportunityRevenue object. Lookup fields should have an API name that begins with 'objectlookup_'.

For example: If you create custom field 'objectlookup_products' in the Campaign Attribution Detail object, and you populate the ObjectLookupID field on the OpportunityRevenue object with a product ID, when the Campaign Attribution Detail object is created for that revenue attribution, the ObjectLookupID that you specified in the OpportunityRevenue object will automatically be stored in the objectlookup_products field of its associated Campaign Attribution Detail object. You would then be able to create reports that take products into account – for example: in addition to analyzing how campaigns attribution opportunities, you can analyze how marketing campaigns attribution specific products.

Different plugin models can use different object lookup types – for example, you can have one model that has a lookup to products, and another that has a lookup to tasks. You should not, however, try mixing different types for a single model – while the application will work, it's not clear that the results will be meaningful. 

You can only add one lookup field for each object type on the Campaign Attribution Detail object. For example, if your Campaign Attribution Detail object already has the field objectlookup_products which is a lookup to Products, you cannot add another objectlookup_ field that is a Products lookup. The application won't be able to determine which field to populate in this case.

The OpportunityDetermineAttribution Method

This method is called with the following parameters:

Map<ID, Opportunity> opportunities

This parameter contains a map of the opportunities in this batch. The default number of opportunities is 200, but this number can be adjusted using the Campaign Attribution and Response Database Configuration page. See note below relating to multicurrency organizations.

Map<ID, Map<ID,CampaignMember>> CreatingContactResponses

This parameter contains a map from opportunity IDs, to a map of CampaignMembers. This map is always empty with Full Circle Campaign Attribution. It is included here for compatibility with campaign attribution models under the Full Circle Response Management application, where the map contains CampaignMembers that were active responses at the time the opportunity was created.

Map<ID, Map<ID, CampaignMember>> PrimaryContactResponses

This parameter contains a map from opportunity IDs, to a map of CampaignMembers. The CampaignMembers in this map are those that belong to the primary contacts on the opportunities.

Map<ID, Map<ID, CampaignMember>> OtherContactResponses

This parameter contains a map from opportunity IDs, to a map of CampaignMembers. The CampaignMembers in this map are those that belong to non-primary contacts on the opportunities.

Map<ID, List<OpportunityRevenue>> OpenOpportunityRevenue

This parameter contains a map from CampaignMember ID (key) to a list of OpportunityRevenue objects that define the revenue attribution to the CampaignMember by opportunity for pipeline revenue. This map is empty when the method is called – it is up to you to populate it with the detailed information. 
You can also use a CampaignID to attribute revenue directly to a campaign instead of individual CampaignMember objects.

Map<ID, List<OpportunityRevenue>> ClosedOpportunityRevenue

This parameter contains a map from CampaignMember ID (key) to a list of OpportunityRevenue objects that define the revenue attribution to the CampaignMember by opportunity for actual revenue. This map is empty when the method is called – it is up to you to populate it with the detailed information. 
You can also use a CampaignID to attribute revenue directly to a campaign instead of individual CampaignMember objects.

Map<ID, List<OpportunityRevenue>> LostOpportunityRevenue

This parameter contains a map from CampaignMember ID (key) to a list of OpportunityRevenue objects that define the revenue attribution to the CampaignMember by opportunity for lost revenue. This map is empty when the method is called – it is up to you to populate it with the detailed information. 
You can also use a CampaignID to attribute revenue directly to a campaign instead of individual CampaignMember objects.

String state

The current state string value. Return this value, or a modified value, to set the state for the next call.


When building an opportunity based model, the framework does the heavy lifting for you, querying the CampaignMember objects that relate to the opportunity and assigning them to one of three categories.

The first category includes those CampaignMember objects that were the active response when the opportunity was created. The campaign for these responses is also referred to as the "tipping point" campaign – the campaign that caused the opportunity to be created in the first place. This category only exists in the Full Circle Response Management application, and is provided here to make it easier to create models that work in both systems. 

The second category includes those CampaignMember objects that are from the primary contact on the opportunity. This is where you can see the response history for the primary contact (excluding the creating response).

The third category includes those CampaignMember objects that are from contacts that are non-primary contacts on the opportunity. This is where you can see the response history for the primary contact (excluding the creating response).

The latter two categories are mutually exclusive – a response cannot appear in both the second and third categories.

When using the opportunity based model, you can only attribute revenue to CampaignMember objects that belong to one of these categories. If you need to attribute revenue to other CampaignMember objects, you will need to use the account based model.

To assign revenue you will create OpportunityRevenue objects and add them to the OpenOpportunityRevenue, ClosedOpportunityRevenue and LostOpportunityRevenue maps. Each of these parameters consists of a map where the key is the CampaignMember ID and the value is a list of OpportunityRevenue objects. The code to do this typically is as follows, where theparameter refers to the parameter to which you wish to add an attribution, cm refers to a CampaignMember and op refers to the attributing Opportunity.

Response Management Compatibility Modifications: Change namespace prefix to 'FCRM',

if(theparameter.get(cm.id)==null)
{
    CurrentRevenueList.put(cm.id, new List<FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue>());
}
theparameter.get(cm.id).add(new FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue(op.id, amount));

You can create multiple OpportunityRevenue objects for a given response and opportunity by specifying different objects in the OpportunityLookup ID field. For example, you can create an OpportunityRevenue object for each product on an opportunity.

The CampaignMember objects include field values for most standard fields and for any fields returned by calls to your model's ICampaignAttributionConfiguration. GetCMFieldsToQuery method.

Note: On multi-currency organizations, the opportunity amount field is converted to the corporate currency value. The opportunity ExpectedRevenue is left in the currency setting for the opportunity. If you use ExpectedRevenue as part of your attribution calculation, you must perform your own conversion to the corporate currency. You can use the fcci.FCI_CampaignInfluenceAPI.GetCurrencyConversionMap API function to obtain a map of country ISO code to currency conversion values should you need to convert other fields.

The AccountDetermineAttribution Method

Map<ID, Account> accounts

This parameter contains a map of the accounts in this batch. The default number of accounts is 50, but this number can be adjusted using the Advanced Configuration page. Organizations with smaller numbers of very active accounts will need to use smaller batch sizes.

Map<ID, Map<ID, Opportunity>> opportunities

This parameter contains a map from account IDs, to a map of Opportunities for those accounts. See note below on multicurrency organizations.

Map<ID, List<OpportunityRevenue>> OpenOpportunityRevenue

This parameter contains a map from CampaignMember ID (key) to a list of OpportunityRevenue objects that define the revenue attribution to the CampaignMember by opportunity for pipeline revenue. This map is empty when the method is called – it is up to you to populate it with the detailed information. 
You can also use a CampaignID to attribute revenue directly to a campaign instead of individual CampaignMember objects.

Map<ID, List<OpportunityRevenue>> ClosedOpportunityRevenue

This parameter contains a map from CampaignMember ID (key) to a list of OpportunityRevenue objects that define the revenue attribution to the CampaignMember by opportunity for actual revenue. This map is empty when the method is called – it is up to you to populate it with the detailed information. 
You can also use a CampaignID to attribute revenue directly to a campaign instead of individual CampaignMember objects.

Map<ID, List<OpportunityRevenue>> LostOpportunityRevenue

This parameter contains a map from CampaignMember ID (key) to a list of OpportunityRevenue objects that define the revenue attribution to the CampaignMember by opportunity for lost revenue. This map is empty when the method is called – it is up to you to populate it with the detailed information. 
You can also use a CampaignID to attribute revenue directly to a campaign instead of individual CampaignMember objects.

String state

The current state string value. Return this value, or a modified value, to set the state for the next call.


When building an account based model, the framework batches request by account, and provides you with a map of the opportunities for each account. It is up to you to query for CampaignMember objects based on the criteria of your choice.

Only a limited number of Account fields are queried for the accounts parameter including: ID, Name, AccountNumber, Type, IsPartner and IsPersonAccount. Additional fields can be specified using your model's ICampaignAttributionConfiguration. GetAccountFieldsToQuery method.

The Opportunity fields queried for the opportunities parameter include the Amount, ExpectedRevenue, IsClosed, IsWon, IsDeleted, Type, CampaignId and AccountID fields. Additional fields can be specified using your model's ICampaignAttributionConfiguration. GetOpportunityFieldsToQuery method.

On the AccountDetermineAttribution method, you add a value by first creating an OpportunityRevenue object that defines the source opportunity and attributed amount (and optionally the first/last touch status and object lookup ID), then by adding it to the list associated with a CampaignMember or

Campaign. The code to do this typically is as follows, where theparameter refers to the parameter to which you wish to add an attribution, cm refers to a CampaignMember and op refers to the attributing Opportunity.

if(theparameter.get(cm.id)==null) 
{
    CurrentRevenueList.put(cm.id, new List<FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue>());
}
theparameter.get(cm.id).add(new FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue(op.id, amount));

You can create multiple OpportunityRevenue objects for a given response and opportunity by specifying different objects in the OpportunityLookup ID field. For example, you can create an OpportunityRevenue object for each product on an opportunity.

Be aware that this AccountDetermineAttribution method may be called multiple times with the same account! In some cases, accounts have too many opportunities to process at once, in which case the method is called multiple times on the same account with different opportunities each time. In some cases, the system detects that a limit error occurred, and reattempts the attribution calculation on a smaller number of accounts and opportunities in order to stay within limits. In extreme cases the method may be called for a single opportunity on a single account.

As always, be sure to use best practices in APEX coding to adhere to limits. Organizations with very large account (large numbers of opportunities, contacts and responses on an account) will likely require smaller batch sizes which you can configure through the Advanced Configuration page.

Note: On multi-currency organizations, the opportunity amount field is converted to the corporate currency value. The opportunity ExpectedRevenue field ExpectedRevenue is left in the currency setting for the opportunity. If you use ExpectedRevenue as part of your attribution calculation, you must perform your own conversion to the corporate currency. You can use the FCCI.FCI_CampaignInfluenceAPI.GetCurrencyConversionMap API function to obtain a map of country ISO code to currency conversion values should you need to convert other fields.

All revenue attribution to the OpenOpportunityRevenue, ClosedOpportunityRevenue and LostOpportunityRevenue maps should be in the corporate currency.

Example

The sample code shown earlier illustrates the class implementation for a simple opportunity based model that supports two weighting schemes, 'First Touch' in which all revenue is attributed to the earliest responses on an opportunity, and 'Even Distribution' in which all revenue is distributed evenly to all responses on an opportunity.
In this example, the OpportunityDetermineAttribution method is implemented as follows:

 global string OpportunityDetermineAttribution(
    Map < ID, Opportunity > opportunities,
    Map < ID, Map < ID, CampaignMember >> CreatingContactResponses,
    Map < ID, Map < ID, CampaignMember >> PrimaryContactResponses,
    Map < ID, Map < ID, CampaignMember >> OtherContactResponses,
    Map < ID, List < FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue >> OpenOpportunityRevenue,
    Map < ID, List < FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue >> ClosedOpportunityRevenue,
    Map < ID, List < FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue >> LostOpportunityRevenue,
    String state) {
    
    // In this model we don't distinguish between primary/non primary,
    // so build a single list for all
    Map < ID, Map < ID, CampaignMember >> allResponses = new Map < ID, Map < ID, CampaignMember >> ();
    for (ID opid: PrimaryContactResponses.keyset()) {
        allResponses.put(opid, new Map < ID, CampaignMember > ());
        allResponses.get(opid).putAll(PrimaryContactResponses.get(opid));
    }
    for (ID opid: OtherContactResponses.keyset()) {
        if (!allResponses.containsKey(opid)) allResponses.put(opid, new Map < ID, CampaignMember > ());
        allResponses.get(opid).putAll(OtherContactResponses.get(opid));
    }
    
    // We need to find the first and last touch response on each opportunity
    Map < ID, CampaignMember > firstTouchByOpportunity = new Map < ID, CampaignMember > ();
    Map < ID, CampaignMember > lastTouchByOpportunity = new Map < ID, CampaignMember > ();
    
    // Now Find the firsta and last touch for each opportunity
    for (ID opid: allResponses.keyset()) {
        for (CampaignMember cm: allResponses.get(opid).values()) {
            if (!firstTouchByOpportunity.containsKey(opid) || firstTouchByOpportunity.get(opid).FirstRespondedDate < cm.FirstRespondedDate) firstTouchByOpportunity.put(opid, cm);
            if (!lastTouchByOpportunity.containsKey(opid) || lastTouchByOpportunity.get(opid).FirstRespondedDate > cm.FirstRespondedDate) lastTouchByOpportunity.put(opid, cm);
        }
    }
    
    // Now do the attribution
    if (weighting == 'First Touch') // First touch attribution only
    {
        for (ID opid: firstTouchByOpportunity.keyset()) {
            CampaignMember cm = firstTouchByOpportunity.get(opid);
            Opportunity op = opportunities.get(opid);
            FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue rev = new FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue(opid, (op.Amount == null) ? 0 : op.Amount);
            rev.IsFirstTouchResponse = true;
            if (op.isClosed) {
                if (op.isWon) AddRevenueObjectToMap(ClosedOpportunityRevenue, cm.id, rev);
                else AddRevenueObjectToMap(LostOpportunityRevenue, cm.id, rev);
            } else AddRevenueObjectToMap(OpenOpportunityRevenue, cm.id, rev);
        }
    } else { // Even distribution
        for (ID opid: allResponses.keyset()) {
            Opportunity op = opportunities.get(opid);
            Integer responseCount = allResponses.get(opid).size();
            for (CampaignMember cm: allResponses.get(opid).values()) {
                FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue rev = new FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue(opid, (op.Amount == null) ? 0 : op.Amount / responseCount);
                // Identify first and last touch
                if (firstTouchByOpportunity.get(opid).id == cm.id) rev.IsFirstTouchResponse = true;
                if (lastTouchByOpportunity.get(opid).id == cm.id) rev.IsLastTouchResponse = true;
                if (op.isClosed) {
                    if (op.isWon) AddRevenueObjectToMap(ClosedOpportunityRevenue, cm.id, rev);
                    else AddRevenueObjectToMap(LostOpportunityRevenue, cm.id, rev);
                } else AddRevenueObjectToMap(OpenOpportunityRevenue, cm.id, rev);
            }
        }
    }
    return null;
}
private void AddRevenueObjectToMap(Map <ID,List < FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue >> revenueMap, ID campaignMemberID, FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue rev) {
    if (!revenueMap.containsKey(campaignMemberID)) revenueMap.put(campaignMemberID, new List < FCCI.FCI_CampaignInfluenceAPI.OpportunityRevenue > ());
    revenueMap.get(campaignMemberID).add(rev);
}
  • Was this article helpful?