├── src
├── classes
│ ├── mc_TestData.cls-meta.xml
│ ├── mc_SubscriptionSyncBatch.cls-meta.xml
│ ├── mc_SubscriptionSyncBatchTest.cls-meta.xml
│ ├── mc_SubscriptionSyncHandler.cls-meta.xml
│ ├── mc_SubscriptionSyncHandlerTest.cls-meta.xml
│ ├── mc_SubscriptionSyncHandlerTest.cls
│ ├── mc_SubscriptionSyncHandler.cls
│ ├── mc_SubscriptionSyncBatchTest.cls
│ ├── mc_TestData.cls
│ └── mc_SubscriptionSyncBatch.cls
├── triggers
│ ├── mc_LeadTrigger.trigger-meta.xml
│ ├── mc_ContactTrigger.trigger-meta.xml
│ ├── mc_LeadTrigger.trigger
│ └── mc_ContactTrigger.trigger
├── package.xml
├── profiles
│ └── Admin.profile
└── objects
│ ├── Lead.object
│ ├── Contact.object
│ └── mc_Marketing_Cloud_Sync_Settings__c.object
└── README.md
/src/classes/mc_TestData.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 41.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/mc_SubscriptionSyncBatch.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 41.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/triggers/mc_LeadTrigger.trigger-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 41.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/mc_SubscriptionSyncBatchTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 41.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/mc_SubscriptionSyncHandler.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 41.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/triggers/mc_ContactTrigger.trigger-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 41.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/classes/mc_SubscriptionSyncHandlerTest.cls-meta.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 41.0
4 | Active
5 |
6 |
--------------------------------------------------------------------------------
/src/triggers/mc_LeadTrigger.trigger:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Ben Edwards (ben@edwards.nz)
3 | * @date 2016-05-31
4 | * @description Trigger for the Lead object to process opt-ins and opt-outs
5 | *
6 | * CHANGE LOG
7 | **/
8 | trigger mc_LeadTrigger on Lead (before update) {
9 |
10 | // Only process if enabled in Custom Setting
11 | if (mc_Marketing_Cloud_Sync_Settings__c.getInstance().Lead_Sync__c) {
12 |
13 | // Send to the handling class for processing
14 | mc_SubscriptionSyncHandler.syncEmailSubscriptionMarketingCloud (trigger.new, trigger.oldMap, 'Lead');
15 | }
16 | }
--------------------------------------------------------------------------------
/src/triggers/mc_ContactTrigger.trigger:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Ben Edwards (ben@edwards.nz)
3 | * @date 2016-05-31
4 | * @description Trigger for the Contact object to process opt-ins and opt-outs
5 | *
6 | * CHANGE LOG
7 | **/
8 | trigger mc_ContactTrigger on Contact (before update) {
9 |
10 | // Only process if enabled in Custom Setting
11 | if (mc_Marketing_Cloud_Sync_Settings__c.getInstance().Contact_Sync__c) {
12 |
13 | // Send to the handling class for processing
14 | mc_SubscriptionSyncHandler.syncEmailSubscriptionMarketingCloud (trigger.new, trigger.oldMap, 'Contact');
15 | }
16 | }
--------------------------------------------------------------------------------
/src/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | *
5 | ApexClass
6 |
7 |
8 | *
9 | ApexTrigger
10 |
11 |
12 | Contact.mc_Marketing_Cloud_Sync_Error__c
13 | Contact.mc_Sync_to_Marketing_Cloud__c
14 | Lead.mc_Marketing_Cloud_Sync_Error__c
15 | Lead.mc_Sync_to_Marketing_Cloud__c
16 | CustomField
17 |
18 |
19 | *
20 | CustomObject
21 |
22 |
23 | Admin
24 | Profile
25 |
26 | 36.0
27 |
28 |
--------------------------------------------------------------------------------
/src/profiles/Admin.profile:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 |
5 | true
6 | Contact.mc_Marketing_Cloud_Sync_Error__c
7 | true
8 |
9 |
10 | true
11 | Contact.mc_Sync_to_Marketing_Cloud__c
12 | true
13 |
14 |
15 | true
16 | Lead.mc_Marketing_Cloud_Sync_Error__c
17 | true
18 |
19 |
20 | true
21 | Lead.mc_Sync_to_Marketing_Cloud__c
22 | true
23 |
24 | Salesforce
25 |
26 |
--------------------------------------------------------------------------------
/src/objects/Lead.object:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | mc_Marketing_Cloud_Sync_Error__c
5 | Displays an error message if a Lead Email Opt Out field change failed to sync to Marketing Cloud.
6 | false
7 | Displays an error message if a Lead Email Opt Out field change failed to sync to Marketing Cloud.
8 |
9 | 255
10 | false
11 | false
12 | Text
13 | false
14 |
15 |
16 | mc_Sync_to_Marketing_Cloud__c
17 | false
18 | Controls whether to sync the record to Marketing Cloud for the batch processing.
19 | false
20 | Controls whether to sync the record to Marketing Cloud for the batch processing.
21 |
22 | false
23 | Checkbox
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/objects/Contact.object:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | mc_Marketing_Cloud_Sync_Error__c
5 | Displays an error message if a Contact Email Opt Out field change failed to sync to Marketing Cloud.
6 | false
7 | Displays an error message if a Contact Email Opt Out field change failed to sync to Marketing Cloud.
8 |
9 | 255
10 | false
11 | false
12 | Text
13 | false
14 |
15 |
16 | mc_Sync_to_Marketing_Cloud__c
17 | false
18 | Controls whether to sync the record to Marketing Cloud for the batch processing.
19 | false
20 | Controls whether to sync the record to Marketing Cloud for the batch processing.
21 |
22 | false
23 | Checkbox
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/classes/mc_SubscriptionSyncHandlerTest.cls:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Ben Edwards (ben@edwards.nz)
3 | * @date 2016-05-31
4 | * @description Test class for the triggers and mc_SubscriptionSyncHandler
5 | *
6 | * CHANGE LOG
7 | **/
8 | @isTest
9 | public class mc_SubscriptionSyncHandlerTest {
10 |
11 | /**
12 | * @author Ben Edwards (ben@edwards.nz)
13 | * @description Test scenario of creating a Contact and checking HasOptedOutOfEmail
14 | **/
15 | @isTest
16 | static void testContactTrigger () {
17 |
18 | // Create a new Contact
19 | Contact testContact = mc_TestData.createContact();
20 |
21 | // Set opt-out
22 | testContact.HasOptedOutOfEmail = true;
23 | update testContact;
24 | }
25 |
26 |
27 | /**
28 | * @author Ben Edwards (ben@edwards.nz)
29 | * @description Test scenario of creating a Lead and checking HasOptedOutOfEmail
30 | **/
31 | @isTest
32 | static void testLeadTrigger () {
33 |
34 | // Create a new Lead
35 | Lead testLead = mc_TestData.createLead();
36 |
37 | // Set opt-out
38 | testLead.HasOptedOutOfEmail = true;
39 | update testLead;
40 | }
41 |
42 |
43 | /**
44 | * @author Ben Edwards (ben@edwards.nz)
45 | * @description Create all test data for the test methods
46 | **/
47 | @testSetup
48 | static void testSetupData () {
49 |
50 | // Enable the Lead and Contact auto-sync
51 | mc_Marketing_Cloud_Sync_Settings__c syncSettings = new mc_Marketing_Cloud_Sync_Settings__c(SetupOwnerId = System.Userinfo.getOrganizationId());
52 | syncSettings.Contact_Sync__c = true;
53 | syncSettings.Lead_Sync__c = true;
54 | insert syncSettings;
55 | }
56 |
57 | }
--------------------------------------------------------------------------------
/src/objects/mc_Marketing_Cloud_Sync_Settings__c.object:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hierarchy
4 | Controls whether the auto-sync of Lead and Contact subscription changes should sync to Marketing Cloud.
5 | false
6 |
7 | Contact_Sync__c
8 | true
9 | Enable or disable auto-syncing of HasOptedOutOfEmail changes to Marketing Cloud for the Contact object.
10 | false
11 | Enable or disable auto-syncing of HasOptedOutOfEmail changes to Marketing Cloud for the Contact object.
12 |
13 | false
14 | Checkbox
15 |
16 |
17 | Lead_Sync__c
18 | true
19 | Enable or disable auto-syncing of HasOptedOutOfEmail changes to Marketing Cloud for the Lead object.
20 | false
21 | Enable or disable auto-syncing of HasOptedOutOfEmail changes to Marketing Cloud for the Lead object.
22 |
23 | false
24 | Checkbox
25 |
26 |
27 | Protected
28 |
29 |
--------------------------------------------------------------------------------
/src/classes/mc_SubscriptionSyncHandler.cls:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Ben Edwards (ben@edwards.nz)
3 | * @date 2016-05-31
4 | * @description Handle the pushing of opt-in's and opt-outs to Marketing Cloud
5 | *
6 | * CHANGE LOG
7 | **/
8 | public class mc_SubscriptionSyncHandler {
9 |
10 | /**
11 | * @author Ben Edwards (ben@edwards.nz)
12 | * @description Process the syncing of opt-in/opt-out changes to Marketing Cloud
13 | * @return void
14 | **/
15 | public static void syncEmailSubscriptionMarketingCloud (List newList, Map oldMap, String objectName) {
16 |
17 | // Boolean to control whether the sync batch should be started
18 | Boolean startSyncBatch = false;
19 |
20 | // Iterate over the record set
21 | for (SObject record :newList) {
22 |
23 | // Get the record Id for the dynamic SObject
24 | Id recordId = (Id) record.get('Id');
25 |
26 | // If the email opt out has changed
27 | if (record.get('HasOptedOutOfEmail') != oldMap.get(recordId).get('HasOptedOutOfEmail')) {
28 |
29 | // If there is a record to sync, set a flag on the record. This enables the batch class to pick it up
30 | record.put('mc_Sync_to_Marketing_Cloud__c', true);
31 |
32 | // Set the variable flag to ensure batch is executed immediately from here
33 | startSyncBatch = true;
34 | }
35 | }
36 |
37 | // If there are records to sync, start the batch
38 | if (startSyncBatch) {
39 |
40 | // Start the batch and run in series of 1. As this has to make a single callout per contact or lead (not bulkified)
41 | // Only want to run in small batches
42 | // Also note, this process will only work for users who have an associated Marketing CLoud user account
43 | // for all other users, a scheduled batch will run as system admin to sync the opt-outs
44 | Database.executeBatch(new mc_SubscriptionSyncBatch(objectName), 1);
45 | }
46 |
47 | }
48 |
49 | }
--------------------------------------------------------------------------------
/src/classes/mc_SubscriptionSyncBatchTest.cls:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Ben Edwards (ben@edwards.nz)
3 | * @date 2016-05-31
4 | * @description Test class for the mc_SubscriptionSyncBatch class
5 | *
6 | * CHANGE LOG
7 | **/
8 | @isTest
9 | public class mc_SubscriptionSyncBatchTest {
10 |
11 |
12 | /**
13 | * @author Ben Edwards (ben@edwards.nz)
14 | * @description Test scenario of processing batch for Contacts
15 | **/
16 | @isTest
17 | static void testContactBatchProcessingSubscribe () {
18 |
19 | // Create a new Contact
20 | Contact testContact = mc_TestData.createContact();
21 |
22 | // Set the Contact to sync
23 | testContact.mc_Sync_to_Marketing_Cloud__c = true;
24 | update testContact;
25 |
26 | // Process the batch
27 | Database.executeBatch(new mc_SubscriptionSyncBatch('Contact'));
28 |
29 | // Not asserting any results - the Marketing Cloud package will do it's thing
30 | }
31 |
32 |
33 | /**
34 | * @author Ben Edwards (ben@edwards.nz)
35 | * @description Test scenario of processing batch for Leads
36 | **/
37 | @isTest
38 | static void testLeadBatchProcessingUnsubscribe () {
39 |
40 | // Create a new Lead
41 | Lead testLead = mc_TestData.createLead();
42 |
43 | // Set the Lead to sync
44 | testLead.mc_Sync_to_Marketing_Cloud__c = true;
45 |
46 | // Set Opt Out to test both cases
47 | testLead.HasOptedOutOfEmail = true;
48 | update testLead;
49 |
50 | // Process the batch
51 | Database.executeBatch(new mc_SubscriptionSyncBatch('Lead'));
52 |
53 | // Not asserting any results - the Marketing Cloud package will do it's thing
54 | }
55 |
56 |
57 | /**
58 | * @author Ben Edwards (ben@edwards.nz)
59 | * @description Simple method to test scheduling the class
60 | **/
61 | @isTest
62 | static void testSchedulable () {
63 |
64 | System.schedule('Test Hourly Schedule', '0 0 * * * ?', new mc_SubscriptionSyncBatch('Contact') );
65 | }
66 |
67 | }
--------------------------------------------------------------------------------
/src/classes/mc_TestData.cls:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Ben Edwards (ben@edwards.nz)
3 | * @date 2016-05-31
4 | * @description Generic utility class for generating data for test methods
5 | *
6 | * CHANGE LOG
7 | **/
8 | @isTest
9 | public class mc_TestData {
10 |
11 | /**
12 | * @author Ben Edwards (ben@edwards.nz)
13 | * @description Instantiates and creates a dummy test Contact
14 | * @return Created Contact record
15 | **/
16 | public static Contact createContact () {
17 |
18 | // Create a new Contact
19 | Contact testContact = new Contact();
20 | testContact.FirstName = 'Snoop';
21 | testContact.LastName = 'Dogg';
22 | testContact.Email = 'snoop@dogg.com';
23 | testContact.MobilePhone = '12 1234 7890';
24 | testContact.MailingStreet = '1 Snoop Dogg Street';
25 | testContact.MailingCity = 'Snooptown';
26 | testContact.MailingState = 'California';
27 | testContact.MailingCountry = 'United States';
28 | testContact.MailingPostalCode = '12345';
29 |
30 | // Insert the contact
31 | insert testContact;
32 |
33 | return testContact;
34 | }
35 |
36 |
37 | /**
38 | * @author Ben Edwards (ben@edwards.nz)
39 | * @description Instantiates and creates a dummy test Lead
40 | * @return Created Lead record
41 | **/
42 | public static Lead createLead () {
43 |
44 | // Create a new Lead
45 | Lead testLead = new Lead();
46 | testLead.FirstName = 'Snoop';
47 | testLead.LastName = 'Dogg';
48 | testLead.Company = 'Snoop Enterprises';
49 | testLead.Email = 'snoop@dogg.com';
50 | testLead.MobilePhone = '12 1234 7890';
51 | testLead.Street = '1 Snoop Dogg Street';
52 | testLead.City = 'Snooptown';
53 | testLead.State = 'California';
54 | testLead.Country = 'United States';
55 | testLead.PostalCode = '12345';
56 |
57 | // Insert the lead
58 | insert testLead;
59 |
60 | return testLead;
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Salesforce to Marketing Cloud Subscription Sync
2 | ## Introduction
3 | By default, subscribing and unsubscribing a Lead/Contact in Salesforce and syncing to Marketing Cloud requires the clicking of a Unsubscribe or Resubscribe custom link.
4 |
5 | This process is a bit cumbersome, and also requires the Salesforce user to be synced to a Marketing Cloud user.
6 |
7 | This package provides code for automating changes of the Email Opt Out (HasOptedOutOfEmail) on Lead and Contact objects by subscribing/unsubscribing the subscriber in Marketing Cloud via API. It will attempt a real-time integration initially, but will also retry in 15 minute batches as a API user if the user who initiates the change isn't synced to a Marketing Cloud user.
8 |
9 | ## Installation
10 | Note: This package requires that the Marketing Cloud Connect package (v5+) is installed. More details here:
11 | https://help.marketingcloud.com/en/documentation/integrated_products__crm_and_web_analytic_solutions/marketing_cloud_connector_v5/
12 |
13 |
14 | 1. **Install package to your Org**
15 |
16 | [](https://githubsfdeploy.herokuapp.com/app/githubdeploy/benedwards44/sf-mc-subscription-sync)
17 |
18 |
19 | 2. **Enable sync options via Custom Setting**
20 |
21 | Setup -> Custom Settings -> Click Manage on Marketing Cloud Sync Settings -> New -> Check either (or both) Lead Sync and Contact Sync fields
22 |
23 |
24 | 3. **Schedule batch class**
25 |
26 | This is used to retry any errors and execute the sync as a user who is setup in Marketing Cloud. To schedule the batch, log in as a user that is set up as an API user in Marketing Cloud
27 |
28 | *Schedule Hourly (Contact)*
29 |
30 | `System.schedule('Marketing Cloud Sync - Contact', '0 0 * * * ?', new mc_SubscriptionSyncBatch('Lead'));`
31 |
32 |
33 | *Schedule Hourly (Lead)*
34 |
35 | `System.schedule('Marketing Cloud Sync - Lead', '0 0 * * * ?', new mc_SubscriptionSyncBatch('Contact'));`
36 |
37 |
38 |
39 | 4. **Add Marketing Cloud Sync Error custom fields to Lead and Contact layouts (OPTIONAL)**
40 |
41 | This allows users or administrators to check if there was a failure in syncing to Marketing Cloud.
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/classes/mc_SubscriptionSyncBatch.cls:
--------------------------------------------------------------------------------
1 | /**
2 | * @author Ben Edwards (ben@edwards.nz)
3 | * @date 2016-05-31
4 | * @description Batch class to execute the physical callout of the update to Marketing Cloud
5 | *
6 | * CHANGE LOG
7 | **/
8 | public class mc_SubscriptionSyncBatch implements Database.Batchable, Schedulable, Database.AllowsCallouts {
9 |
10 | // The object to process for the batch
11 | public final String objectType;
12 |
13 | // CONSTRUCTOR
14 | public mc_SubscriptionSyncBatch (String objType) {
15 |
16 | // Assign the object type from the constructor variable
17 | objectType = objType;
18 | }
19 |
20 | /*
21 | BATCH CLASS METHODS
22 | */
23 |
24 | // The Start of the Batch
25 | public Database.QueryLocator start(Database.BatchableContext BC) {
26 |
27 | // Query all contacts that require to be synced
28 | return Database.getQueryLocator('SELECT Id, HasOptedOutOfEmail FROM ' + objectType + ' WHERE mc_Sync_to_Marketing_Cloud__c = true');
29 | }
30 |
31 | // Execute the job logic
32 | public void execute(Database.BatchableContext BC, List scope) {
33 |
34 | // Iterate over contacts
35 | for (SObject recordTypeSync :scope) {
36 |
37 | // Execute the opt-out sync and capture result
38 | String result = executeUnSubReSubCallout ((Id) recordTypeSync.get('Id'), (Boolean) recordTypeSync.get('HasOptedOutOfEmail'));
39 |
40 | // If the contact is successfully synced
41 | if (result == 'success' || result == 'not found') {
42 |
43 | // Set sync flag to false, so it doesn't get picked up again
44 | recordTypeSync.put('mc_Sync_to_Marketing_Cloud__c', false);
45 | recordTypeSync.put('mc_Marketing_Cloud_Sync_Error__c', null);
46 | }
47 | else {
48 |
49 | // Set any errors against the contact
50 | recordTypeSync.put('mc_Marketing_Cloud_Sync_Error__c', result);
51 | }
52 |
53 | // If failed, leave the flag as true as it will get picked up in a scheduled batch and retried.
54 | }
55 |
56 | // Update the contacts so they aren't synced again
57 | update scope;
58 | }
59 |
60 | // Finish silently
61 | public void finish(Database.BatchableContext BC) {}
62 |
63 | /*
64 | SCHEDULABLE METHODS
65 | */
66 |
67 | // The Schedulable execute method
68 | public void execute(SchedulableContext sc) {
69 |
70 | // Start the batch and run in seriers of 5. As this has to make a single callout per contact (not bulkified)
71 | // Only want to run in small batches
72 | Database.executeBatch(new mc_SubscriptionSyncBatch(objectType), 1);
73 | }
74 |
75 |
76 |
77 | /**
78 | * @author Ben Edwards (ben@cloudinit.nz)
79 | * @description Execute callout asyncronously to MC API
80 | **/
81 | private String executeUnSubReSubCallout (Id recordId, Boolean isUnsub) {
82 |
83 | // Variable to capture the sync result. Set to failed.
84 | // The sync method will update to success if successful
85 | String syncResult = 'Not Started';
86 |
87 | try {
88 |
89 | // Different methods for unsubscribe and re-subscribe
90 | if (isUnsub) {
91 |
92 | // Attempt unsub integration
93 | syncResult = et4ae5.jsButtonMethods.performUnsub(recordId, objectType);
94 | }
95 | else {
96 |
97 | // Attempt re-sub integration
98 | syncResult = et4ae5.jsButtonMethods.performResub(recordId, objectType);
99 | }
100 | }
101 | catch (Exception ex) {
102 |
103 | // Set the result to failed for any errors
104 | syncResult = String.valueOf(ex);
105 |
106 | // Capture failure in debug
107 | system.debug('### Error syncing Contact to Marketing Cloud: ' + ex);
108 | }
109 |
110 | return syncResult;
111 | }
112 |
113 | }
--------------------------------------------------------------------------------