Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contacts two way sync #6

Merged
merged 37 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7f53408
contact sync start
martinfbluesoftcz Jan 2, 2024
43aefa6
contacts wip
martinfbluesoftcz Jan 2, 2024
e326dc5
Merge remote-tracking branch 'origin/dev-leads-phase1' into contacts-…
martinfbluesoftcz Jan 3, 2024
f63943a
contact sync wip
martinfbluesoftcz Jan 3, 2024
5a4029a
contacts sync wip
martinfbluesoftcz Jan 5, 2024
9ff85b7
Merge branch 'main' into contacts-two-way-sync
martinfbluesoftcz Jan 7, 2024
8380235
Merge branch 'form-leads-finished' into contacts-two-way-sync
martinfbluesoftcz Jan 16, 2024
ce68754
cln
martinfbluesoftcz Jan 16, 2024
52bb44f
wip
martinfbluesoftcz Jan 17, 2024
ba579d0
Merge branch 'form-leads-finished' into contacts-two-way-sync
martinfbluesoftcz Jan 17, 2024
d64af9b
Merge branch 'main' into contacts-two-way-sync
martinfbluesoftcz Jan 24, 2024
430cb73
contact sync wip
martinfbluesoftcz Jan 24, 2024
3db0123
Merge remote-tracking branch 'origin/main' into contacts-two-way-sync
martinfbluesoftcz Jan 24, 2024
26b1339
salesforce contacts api
martinfbluesoftcz Jan 25, 2024
083239c
contacts wip
martinfbluesoftcz Jan 25, 2024
f3de960
Merge remote-tracking branch 'origin/main' into contacts-two-way-sync
martinfbluesoftcz Jan 26, 2024
7caef6b
contacts finished
martinfbluesoftcz Jan 26, 2024
cd9ba0f
Merge remote-tracking branch 'origin/main' into contacts-two-way-sync
martinfbluesoftcz Jan 28, 2024
a2d2b57
new setting property, fixes after testing
martinfbluesoftcz Jan 28, 2024
ee9e19b
datetime offset fix, new class for contacts sync time wip
martinfbluesoftcz Jan 29, 2024
55467ea
Merge remote-tracking branch 'origin/main' into contacts-two-way-sync
martinfbluesoftcz Jan 30, 2024
254bbc2
contact last sync custom class, fixes
martinfbluesoftcz Jan 30, 2024
8855022
Merge remote-tracking branch 'origin/main' into contacts-two-way-sync
martinfbluesoftcz Feb 2, 2024
469a73c
installer change for new class
martinfbluesoftcz Feb 2, 2024
0338d35
main readme for contacts, test samples, email fix
martinfbluesoftcz Feb 4, 2024
f4cce2a
warnings removed
martinfbluesoftcz Feb 4, 2024
54cbfd6
user guide for contacts
martinfbluesoftcz Feb 5, 2024
6617d7d
fix typo in folder structure
martinfbluesoftcz Feb 5, 2024
41ee563
listing page for synced contacts
martinfbluesoftcz Feb 6, 2024
379ebce
allow null for FailedSyncItemNextTime
martinfbluesoftcz Feb 6, 2024
739267d
Merge remote-tracking branch 'origin/main' into contacts-two-way-sync
martinfbluesoftcz Feb 13, 2024
75d08f0
changes after code review
martinfbluesoftcz Feb 13, 2024
d97012f
removed warnigns and comment in program.cs
martinfbluesoftcz Feb 15, 2024
efa5653
custom classes as System to show them in CMS, contacts sync table scr…
martinfbluesoftcz Feb 21, 2024
7f6653c
typo fix
martinfbluesoftcz Feb 21, 2024
e8309ef
user guide ToC
martinfbluesoftcz Feb 21, 2024
2371b55
Version bump 2.0.0-reprelease
ondrejhenek Mar 14, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading