Skip to content

Commit

Permalink
Merge pull request #6 from Kentico/contacts-two-way-sync
Browse files Browse the repository at this point in the history
Contacts two way sync, breaking change in DB
  • Loading branch information
ondrejhenek authored Mar 14, 2024
2 parents 19aa8c7 + 2371b55 commit 3470f76
Show file tree
Hide file tree
Showing 78 changed files with 3,866 additions and 226 deletions.
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<Authors>$(Company)</Authors>
<Copyright>Copyright © $(Company) $([System.DateTime]::Now.Year)</Copyright>
<Trademark>$(Company)™</Trademark>
<VersionPrefix>1.0.1</VersionPrefix>
<VersionSuffix></VersionSuffix>
<VersionPrefix>2.0.0</VersionPrefix>
<VersionSuffix>prerelease1</VersionSuffix>
<PackageLicenseExpression>MIT</PackageLicenseExpression>

<PackageProjectUrl>https://github.com/Kentico/xperience-by-kentico-crm</PackageProjectUrl>
Expand Down
33 changes: 28 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ Added form with auto mapping based on Form field mapping to Contacts atttibutes.
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// ...
builder.Services.AddKenticoCRMDynamics(builder =>
builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME));
Expand All @@ -84,7 +83,6 @@ Example how to add form with own mapping:
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// ...
builder.Services.AddKenticoCRMDynamics(builder =>
builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name
Expand All @@ -103,7 +101,6 @@ Use this option when you need complex logic and need to use another service via
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// ...
builder.Services.AddKenticoCRMDynamics(builder =>
builder.AddFormWithConverter<SomeCustomConverter>(DancingGoatContactUsItem.CLASS_NAME));
Expand All @@ -117,7 +114,6 @@ Added form with auto mapping based on Form field mapping to Contacts atttibutes.
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// ...
builder.Services.AddKenticoCRMSalesforce(builder =>
builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME));
Expand All @@ -129,7 +125,6 @@ Example how to add form with own mapping:
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// ...
builder.Services.AddKenticoCRMSalesforce(builder =>
builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name
Expand All @@ -154,6 +149,34 @@ Use this option when you need complex logic and need to use another service via
builder.AddFormWithConverter<SomeCustomConverter>(DancingGoatContactUsItem.CLASS_NAME));
```

### Contacts integration
You can enable synchronization of online marketing contacts (OM_Contact table).
You can choose between Lead and Contact entities in CRM where to sync data (but only one option is supported at any given time).

#### Dynamics Sales

```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Choose between sync to Leads and Contacts (only one option is supported)!
// Add sync to Leads
builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Lead);
// Add sync to Contacts
builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Contact);
```

#### Salesforce

```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Choose between sync to Leads and Contacts (only one option is supported)!
// Add sync to Leads
builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Lead);
// Add sync to Contacts
builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Contact);
```

## Full Instructions

View the [Usage Guide](./docs/Usage-Guide.md) for more detailed instructions.
Expand Down
245 changes: 237 additions & 8 deletions docs/Usage-Guide.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
# Usage Guide

## Table of contents
1. [Screenshots](#screenshots)
2. [CRM settings](#crm-settings)
3. [Forms data - Leads integration](#forms-data---leads-integration)
4. [Contacts integration](#contacts-integration)
5. [Troubleshooting](#troubleshooting)

## Screenshots

![Synchronized leads](../images/screenshots/CRM_form_sync_table.png "Table of synchronized leads")
![Synchronized contacts](../images/screenshots/CRM_contacts_sync_table.png "Table of synchronized contacts")
![Dynamics settings](../images/screenshots/Dynamics_CRM_settings.png "Dynamics CRM settings")

## CRM settings
Expand All @@ -20,14 +28,15 @@ Integration uses OAuth client credentials scheme, so you have to setup your CRM

### CRM settings description

| Setting | Description |
| ----------------------- | ------------------------------------------------------------------------------------ |
| Forms enabled | If enabled form submissions for registered forms are sent to CRM Leads |
| Contacts enabled (TBD) | If enabled online marketing contacts are synced to CRM Leads or Contacts |
| Ignore existing records | If enabled then no updates in CRM will be performed on records with same ID or email |
| CRM URL | Base Dynamics / Salesforce instance URL |
| Client ID | Client ID for OAuth 2.0 client credentials scheme |
| Client secret | Client secret for OAuth 2.0 client credentials scheme |
| Setting | Description |
|-------------------------------| ------------------------------------------------------------------------------------ |
| Forms enabled | If enabled form submissions for registered forms are sent to CRM Leads |
| Contacts enabled | If enabled online marketing contacts are synced to CRM Leads or Contacts |
| Contacts two-way sync enabled | If enabled contacts are synced from CRM to Kentico (can set only when previous 'Contacts enabled' setting is true)
| Ignore existing records | If enabled then no updates in CRM will be performed on records with same ID or email |
| CRM URL | Base Dynamics / Salesforce instance URL |
| Client ID | Client ID for OAuth 2.0 client credentials scheme |
| Client secret | Client secret for OAuth 2.0 client credentials scheme |

### Dynamics settings

Expand Down Expand Up @@ -220,3 +229,223 @@ Use this option when you need complex logic and need to use another service via
builder.Services.AddKenticoCRMSalesforce(builder =>
builder.AddFormWithConverter<SomeCustomConverter>(DancingGoatContactUsItem.CLASS_NAME));
```

## Contacts integration

You can enable synchronization of online marketing contacts (OM_Contact table).
You can choose between Lead and Contact entities in CRM where to sync data (but only one option is supported at any given time).

### Dynamics Sales

Basic example how to init (default mapping from ContactInfo to CRM entity is used):

```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Choose between sync to Leads and Contacts (only one option is supported)!
// Add sync to Leads
builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Lead);
// Add sync to Contacts
builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Contact);
```

Example how to init sync to Leads with custom mapping:

```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// ...
// Choose between sync to Leads and Contacts (only one option is supported)!
builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Lead, builder =>
builder.MapField(nameof(ContactInfo.ContactEmail), "emailaddress1")
.MapField(c => c.ContactFirstName, "firstname")
.MapField<Lead>(nameof(ContactInfo.ContactLastName), e => e.LastName)
.MapField<Lead>(c => c.ContactMobilePhone, e => e.MobilePhone),
useDefaultMappingToCRM: false);
```

For most advanced scenarios when you need to use injected services, custom converters are recommended:

First create custom converter (example from ContactInfo to Lead):

```csharp
public class DynamicsContactToLeadCustomConverter : ICRMTypeConverter<ContactInfo, Lead>
{
public Task Convert(ContactInfo source, Lead destination)
{
//to do some mapping
destination.EMailAddress1 = source.ContactEmail;
// ...
return Task.CompletedTask;
}
}
```
Then initialize integration with custom converter:
```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// ...
// Sync to Leads (only one option is supported)!
builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Lead, builder =>
builder.AddContactToLeadConverter<DynamicsContactToLeadCustomConverter>(),
useDefaultMappingToCRM: false); // when true default mapping is applied after custom converter
// Sync to Contacts (only one option is supported)!
builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Contact, builder =>
builder.AddContactToContactConverter<DynamicsContactToContactCustomConverter>(),
useDefaultMappingToCRM: false); // when true default mapping is applied after custom converter
```

#### Sync from CRM to Kentico

Contacts are synced each minute from CRM (from Leads or Contacts) when setting 'Contacts two-way sync enabled' is checked.
By default existing contacts (paired by email) are updated and new contacts are created (default mapping is used).
Update is performed only when some data has changed.\
But you can customize this process with custom converter:

```csharp
public class DynamicsContactToKenticoContactCustomConverter : ICRMTypeConverter<Contact, ContactInfo>
{
public Task Convert(Contact source, ContactInfo destination)
{
if (destination.ContactID == 0)
{
// mapping on create
destination.ContactEmail = source.EMailAddress1;
destination.ContactFirstName = source.FirstName;
destination.ContactLastName = source.LastName;
}
else
{
// mapping on update
destination.ContactNotes = $"Status: {source.StatusCode?.ToString()}";
}

return Task.CompletedTask;
}
}
```

```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// ...
builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Contact, builder =>
builder.AddContactToKenticoConverter<DynamicsContactToKenticoContactCustomConverter>(),
useDefaultMappingToKentico: false); // when true then both (custom and default) converter are applied
```

### Salesforce

Basic example how to init (default mapping from ContactInfo to CRM entity is used):
```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Choose between sync to Leads and Contacts (only one option is supported)!
// Add sync to Leads
builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Lead);
// Add sync to Contacts
builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Contact);
```

Example how to init sync to Leads with custom mapping:

```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// ...
// Choose between sync to Leads and Contacts (only one option is supported)!
builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Lead, builder =>
builder.MapField(nameof(ContactInfo.ContactEmail), "Email")
.MapField(c => c.ContactFirstName, "FirstName")
.MapLeadField(nameof(ContactInfo.ContactLastName), e => e.LastName)
.MapLeadField(c => c.ContactMobilePhone, e => e.MobilePhone),
useDefaultMappingToCRM: false);
```

For most advanced scenarios when you need to use injected services, custom converters are recommended:

First create custom converter (example from ContactInfo to Lead):

```csharp
public class SalesforceContactToLeadCustomConverter : ICRMTypeConverter<ContactInfo, LeadSObject>
{
public Task Convert(ContactInfo source, LeadSObject destination)
{
//to do some mapping
destination.Email = source.ContactEmail;

return Task.CompletedTask;
}
}
```
Then initialize integration with custom converter:
```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// ...
// Sync to Leads (only one option is supported)!
builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Lead, builder =>
builder.AddContactToLeadConverter<SalesforceContactToLeadCustomConverter>(),
useDefaultMappingToCRM: false); // when true default mapping is applied after custom converter
// Sync to Contacts (only one option is supported)!
builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Lead, builder =>
builder.AddContactToContactConverter<SalesforceContactToContactCustomConverter>(),
useDefaultMappingToCRM: false); // when true default mapping is applied after custom converter
```

#### Duplicates detection issue

By default Salesforce has duplicates detection enabled. Collisions can be detected even between records in Leads and Contacts.\
For this reason, we do not recommend using the synchronization of form submissions and synchronization of contacts at the same time unless duplicate detection is turned off.\
More about [Standard Duplicate Rules](https://help.salesforce.com/s/articleView?id=sf.duplicate_rules_standard_rules.htm&type=5)

#### Sync from CRM to Kentico

Contacts are synced each minute from CRM (from Leads or Contacts) when setting 'Contacts two-way sync enabled' is checked.
By default existing contacts (paired by email) are updated and new contacts are created (default mapping is used).
Update is performed only when some data has changed.\
But you can customize this process with custom converter:

```csharp
public class SalesforceLeadToKenticoContactCustomConverter : ICRMTypeConverter<LeadSObject, ContactInfo>
{
public Task Convert(LeadSObject source, ContactInfo destination)
{
if (destination.ContactID == 0)
{
// mapping on create
destination.ContactEmail = source.Email;
destination.ContactFirstName = source.FirstName;
destination.ContactLastName = source.LastName;
}
else
{
// mapping on update
destination.ContactNotes = $"Status: {source.Status}";
}

return Task.CompletedTask;
}
}}
```

```csharp
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// ...
builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Lead, builder =>
builder.AddLeadToKenticoConverter<SalesforceLeadToKenticoContactCustomConverter>(),
useDefaultMappingToKentico: false); // when true then both (custom and default) converter are applied
```

## Troubleshooting

- When uprading from version 1.0.0 and database objects has been already created, you need to manually change
[FailedSyncItemNextTime] column as nullable in table [KenticoCRMCommon_FailedSyncItem] to prevent errors in thread worker after
10 failed attempts were performed.
\
Another solution for this issue is drop table and remove record in CMS_Class (ClassName: KenticoCRMCommon.FailedSyncItem) and let install table again after restarting application.


18 changes: 18 additions & 0 deletions examples/DancingGoat/Controllers/TestController.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using CMS.OnlineForms;
using CMS.OnlineForms.Types;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using JsonSerializer = System.Text.Json.JsonSerializer;

namespace DancingGoat.Controllers;

Expand All @@ -14,4 +16,20 @@ public IActionResult FormLead(int id)
item.Update();
return Ok();
}

public IActionResult TestDate()
{
string jsonString = "{\"CreatedDate\":\"2024-01-28T16:43:35.000+0000\"}";
var myObject = JsonConvert.DeserializeObject<MyObject>(jsonString);
var myObject2 = JsonSerializer.Deserialize<MyObject>(jsonString);

Console.WriteLine($"CreatedDate: {myObject.CreatedDate}");
return Ok();
}

public class MyObject
{
[JsonProperty("CreatedDate")]
public DateTimeOffset CreatedDate { get; set; }
}
}
Loading

0 comments on commit 3470f76

Please sign in to comment.