Sync efforts fall into four categories:
- Push from CiviCRM to Mailchimp.
- Pull from Mailchimp to CiviCRM.
- CiviCRM-fired hooks.
- Mailchimp-fired Webhooks
Note that a key difference between push and pull, other than the direction of authority, is that mapped interest groups can be declared as being allowed to be updated on a pull or not. This is useful when Mailchimp has no right to change that interest group, e.g. a group that you identify with a smart group in CiviCRM. Typically such groups should be considered internal and therefore hidden from subscribers at all times.
One of the challenges is to identify the CiviCRM contact that a mailchimp
member matches. The code for this is centralised in
CRM_Mailchimp_Sync::guessContactIdSingle()
, which has tests at
MailchimpApiMockTest::testGuessContactIdSingle()
.
Look at the comment block for that test and for the guessContactIdSingle
method for details of how contacts are identified. However, this is slow and so
for the bulk operations there's some SQL shortcuts for efficiency which are in the methods:
guessContactIdsBySubscribers
guessContactIdsByNameAndEmail
guessContactIdsByUniqueEmail
Mailchimp lists default to having FNAME
and LNAME
merge fields to store
first and last names. Some people change/delete these merge fields which makes
things difficult. A common reason is that people wanted a single name field on a
Mailchimp-provided sign-up form. This extension allows for the existance of a
NAME
merge field. Names found here are split automatically (on spaces) with
the first word becomming the first name and any names following being used as
last names. See unit tests for conditions and handling of blanks.
A 'pull' sync will split the names and then work as if those names were in FNAME, LNAME merge fields, but only if the FNAME/LNAME fields don't exist or are both empty.
A 'push' sync will combine the first and last names into a single string and
submit that to the NAME
merge field, if it exists.
In order to be subscribed, the contact must:
- have an email available
- not be deceased
- not have
is_opt_out
set - not have
do_not_email
set
In terms of subscribing people from CiviCRM to Mailchimp, it will use the first available (i.e. not "on hold") email in this order:
- Specified bulk email address
- Primary email address
- Any other email address
-
tests in tests/phpunit are designed to be run automatically, e.g. by CI. Within this dir there are unit tests that are proper unit tests (i.e. tests that do not rely on anything other than the system under test; no dependencies; no connection to a database or external service.) and integration tests that do depend on the CiviCRM database, but none of these test use the actual Mailchimp API; none of them require a Mailchimp account. Some of the tests mock the Mailchimp API with Prophesy.
-
tests in tests/integration are NOT designed to run automatically and do require a Mailchimp account, properly configured in CiviCRM. You need to run these in a special way. Ideally they would be rewritten to use the
cv
program to bootstrap Civi in a way that would work for Wordpress and Drupal, currently the boostrapping is hackish. PRs welcome :-)
To run tests, install a Civi Build Kit installation, then from the extension's dir, run one of these:
phpunit5 tests/phpunit/unit/
phpunit5 tests/phpunit/MailchimpApiMockTest.php
phpunit5 tests/phpunit/SyncIntegrationTest.php
phpunit5 tests/phpunit/CRM/Mailchimp/IntegrationTest.php
phpunit5 tests/phpunit/CRM/Mailchimp/WebhookSecurityTest.php
The Push Sync is done by the CRM_Mailchimp_Sync
class. The steps are:
- Fetch required data from Mailchimp for all the list's members.
- Fetch required data from CiviCRM for all the list's CiviCRM membership group.
- Add those who are not on Mailchimp, and update those whose details are different on CiviCRM compared to Mailchimp.
- Remove from mailchimp those not on CiviCRM.
The test cases are as follows:
testPushAddsNewPerson()
checks this.
CiviCRM Mailchimp Result (at Mailchimp)
--------+----------+---------------------
Fred Fred (added)
Fred Fred Fred (no change)
Fred Barney Fred (corrected)
Fred Fred (no change)
--------+----------+---------------------
This logic is tested by tests/unit/SyncTest.php
The collection, comparison and API calls are tested in
tests/integration/MailchimpApiIntegrationTest.php
This logic is tested by tests/unit/SyncTest.php
The collection, comparison and API calls are tested in
tests/integration/MailchimpApiIntegrationTest.php
This is tested in tests/integration/MailchimpApiIntegrationTest.php
in testPushUnsubscribes()
This is tested in tests/integration/MailchimpApiIntegrationTest.php
in testPushUnsubscribes()
The Pull Sync is done by the CRM_Mailchimp_Sync
class. The steps are:
- Fetch required data from Mailchimp for all the list's members.
- Fetch required data from CiviCRM for all the list's CiviCRM membership group.
- Identify a single contact in CiviCRM that corresponds to the Mailchimp member, create a contact if needed.
- Update the contact with name and interest group changes (only for interests that are configured to allow Mailchimp to CiviCRM updates)
- Remove contacts from the membership group if they are not subscribed at Mailchimp.
The test cases are as follows:
An email from Mailchimp can be used to identify the CiviCRM contact if if matches among a list of CiviCRM contacts that are in the membership group.
This is done with SyncIntegrationTest::testGuessContactIdsBySubscribers
An email can be matched if it's unique to a particular contact in CiviCRM.
This is done with SyncIntegrationTest::testGuessContactIdsByUniqueEmail
An email can be matched along with a first and last name if they all match only one contact in CiviCRM.
This is done with SyncIntegrationTest::testGuessContactIdsByNameAndEmail
See integration test testPullChangesName()
and for the name logic see unit test
testUpdateCiviFromMailchimpContactLogic
.
See integration tests:
testPullChangesInterests()
For when the group is configured with update permission from Mailchimp to Civi.testPullChangesNonPullInterests()
For when the group is NOT configured with update permission.
See integration test testPullAddsContact()
.
Test that contacts not received from Mailchimp but in membership group get removed from membership group.
See integration test testPullRemovesContacts()
.
Mailchimp's webhooks are an important part of the system. If they are functioning correctly then the Pull sync should never need to make any changes.
But they're a nightmare for non-techy users to configure, so now this extension takes care of them. When you visit the settings page all groups' webhooks are checked, with errors shown to the user. You can correct a list's webhooks by editing the CiviCRM group settings. There's a tickbox for doing the webhook changes which defaults to ticked, and when you save it will ensure everything is correct.
Tests
MailchimpApiMockTest::testCheckGroupsConfig
MailchimpApiMockTest::testConfigureList
If you add/remove/delete a single contact from a group that is associated with a Mailchimp list then the posthook is used to detect this and make the change at Mailchimp.
There are several cases that this does not cover (and it's therefore of questionable use):
-
Smart groups. If you have a smart group of all with last name Flintstone and you change someone's name to Flintstone, thus giving them membership of that group, this hook will not be triggered (@todo test).
-
Block additions. If you add more than one contact to a group, the immediate Mailchimp updates are not triggered. This is because each contact requires a separate API call. Add thousands and this will cause big problems.
If the group you added someone to was synced to an interest at Mailchimp then the person's membership is checked. If they are, according to CiviCRM in the group mapped to that lists's membership, then their interests are updated at Mailchimp. If they are not currently in the membership CiviCRM group then the interest change is not attempted to be registered with Mailchimp.
See Tests:
MailchimpApiMockTest::testPostHookForMembershipListChanges()
MailchimpApiMockTest::testPostHookForInterestGroupChanges()
Because of these limitations, you cannot rely on this hook to keep your list up-to-date and will always need to do a CiviCRM to Mailchimp Push sync before sending a mailing.
The settings page stores details like the API key etc.
However it also serves to check the mapped groups and lists are properly set up. Specifically it:
- Checks that the list still exists on Mailchimp
- Checks that the list's webhook is set and configured exactly.
Warnings are displayed on screen when these settings are wrong and these include a link to the group's settings page, from which you can auto-configure the list to the correct settings on Save.
These warnings are tested in MailchimpApiMockTest::testCheckGroupsConfig()
.
One thing we can't cope with is duplicate contacts. This is now fairly rare because of the more liberal matching of CiviCRM contacts in version 2.0.
Specifically: an email coming from Mailchimp belonged to several contacts and we were unable to narrow it down by the names (perhaps there was no name in Mailchimp).
On push, the temporary mailchimp table has NULL in it for these contacts. Normally we would unsubscribe emails from Mailchimp that are not matched in the CiviCRM table, but we'll avoid unsubscribing the ones that are NULL.
On pull, we will not create a contact for NULL cid_guess
records.
This means that the contact will stay on Mailchimp unaffected and un-synced by any sync operations. They are therefore un-sync-able.
The alternatives?
-
create new contact. Can't do this; it could result in creating a new contact on every sync, since every creation would cause the duplication to increase.
-
pick one of the matching contacts at random but they could be different people sharing an email so we wouldn't want to merge in any names or interests based on the wrong contact.