From 7f53408d90b16a8246b77b692ca3499c6194a28b Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Tue, 2 Jan 2024 15:21:04 +0100 Subject: [PATCH 01/26] contact sync start --- .../Configuration/ContactMappingBuilder.cs | 30 +++++++ .../ContactMappingConfiguration.cs | 8 ++ .../Mapping/IContactFieldMapping.cs | 8 ++ .../ContactFieldMappingFunction.cs | 15 ++++ .../ContactFieldNameMapping.cs | 20 +++++ .../ContactFieldToCRMMapping.cs | 15 ++++ .../ServiceCollectionExtensions.cs | 20 ++++- .../Services/ILeadsIntegrationService.cs | 17 +++- .../BizFormFieldsMappingBuilderExtensions.cs | 25 +----- .../ContactMappingBuilderExtensions.cs | 85 +++++++++++++++++++ .../DynamicsContactMappingConfiguration.cs | 7 ++ .../DynamicsServiceCollectionExtensions.cs | 17 +++- .../Helpers/EntityHelper.cs | 27 ++++++ .../SalesForceServiceCollectionsExtensions.cs | 2 +- 14 files changed, 269 insertions(+), 27 deletions(-) create mode 100644 src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Mapping/IContactFieldMapping.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldMappingFunction.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldNameMapping.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldToCRMMapping.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingConfiguration.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Helpers/EntityHelper.cs diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs new file mode 100644 index 0000000..527b01b --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs @@ -0,0 +1,30 @@ +using CMS.ContactManagement; +using Kentico.Xperience.CRM.Common.Mapping.Implementations; + +namespace Kentico.Xperience.CRM.Common.Configuration; + +public class ContactMappingBuilder +{ + private List fieldMappings = new(); + + public ContactMappingBuilder MapField(string contactFieldName, string crmFieldName) + { + fieldMappings.Add(new ContactFieldToCRMMapping(new ContactFieldNameMapping(contactFieldName), new CRMFieldNameMapping(crmFieldName))); + return this; + } + + public ContactMappingBuilder MapField(Func mappingFunc, string crmFieldName) + { + fieldMappings.Add(new ContactFieldToCRMMapping(new ContactFieldMappingFunction(mappingFunc), new CRMFieldNameMapping(crmFieldName))); + return this; + } + + internal TContactMappingConfiguration Build() + where TContactMappingConfiguration : ContactMappingConfiguration, new() + { + return new TContactMappingConfiguration + { + FieldsMapping = fieldMappings + }; + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs new file mode 100644 index 0000000..834680e --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs @@ -0,0 +1,8 @@ +using Kentico.Xperience.CRM.Common.Mapping.Implementations; + +namespace Kentico.Xperience.CRM.Common.Configuration; + +public class ContactMappingConfiguration +{ + public List FieldsMapping { get; internal init; } = new(); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Mapping/IContactFieldMapping.cs b/src/Kentico.Xperience.CRM.Common/Mapping/IContactFieldMapping.cs new file mode 100644 index 0000000..309cc06 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Mapping/IContactFieldMapping.cs @@ -0,0 +1,8 @@ +using CMS.ContactManagement; + +namespace Kentico.Xperience.CRM.Common.Mapping; + +public interface IContactFieldMapping +{ + object MapContactField(ContactInfo contactInfo); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldMappingFunction.cs b/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldMappingFunction.cs new file mode 100644 index 0000000..5b96e51 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldMappingFunction.cs @@ -0,0 +1,15 @@ +using CMS.ContactManagement; + +namespace Kentico.Xperience.CRM.Common.Mapping.Implementations; + +public class ContactFieldMappingFunction : IContactFieldMapping +{ + private readonly Func mappingFunc; + + public ContactFieldMappingFunction(Func mappingFunc) + { + this.mappingFunc = mappingFunc; + } + + public object MapContactField(ContactInfo contactInfo) => mappingFunc(contactInfo); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldNameMapping.cs b/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldNameMapping.cs new file mode 100644 index 0000000..337447b --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldNameMapping.cs @@ -0,0 +1,20 @@ +using CMS.ContactManagement; +using CMS.OnlineForms; + +namespace Kentico.Xperience.CRM.Common.Mapping.Implementations; + +/// +/// Contact Info item field mapping based on form field name +/// +public class ContactFieldNameMapping : IContactFieldMapping +{ + private readonly string contactFieldName; + + public ContactFieldNameMapping(string contactFieldName) + { + this.contactFieldName = contactFieldName; + } + + public object MapContactField(ContactInfo contactInfo) + => contactInfo.GetValue(contactFieldName); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldToCRMMapping.cs b/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldToCRMMapping.cs new file mode 100644 index 0000000..2ac086c --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldToCRMMapping.cs @@ -0,0 +1,15 @@ +namespace Kentico.Xperience.CRM.Common.Mapping.Implementations; + +/// +/// Mapping wrapper for BizForm field mapping and Crm entity field mapping +/// +public class ContactFieldToCRMMapping +{ + public ContactFieldToCRMMapping(IContactFieldMapping contactFieldMapping, ICRMFieldMapping crmFieldMapping) + { + ContactFieldMapping = contactFieldMapping; + CRMFieldMapping = crmFieldMapping; + } + public IContactFieldMapping ContactFieldMapping { get; } + public ICRMFieldMapping CRMFieldMapping { get; } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs index 9541ef6..fa2527b 100644 --- a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs @@ -19,7 +19,7 @@ public static class ServiceCollectionExtensions /// /// /// - public static IServiceCollection AddKenticoCrmCommonIntegration( + public static IServiceCollection AddKenticoCrmCommonLeadIntegration( this IServiceCollection services, Action formsMappingConfig) where TMappingConfiguration : BizFormsMappingConfiguration, new() { @@ -39,6 +39,24 @@ public static IServiceCollection AddKenticoCrmCommonIntegration( + this IServiceCollection services, Action contactMappingConfig) + where TMappingConfiguration : ContactMappingConfiguration, new() + { + services.TryAddSingleton( + _ => + { + var mappingBuilder = new ContactMappingBuilder(); + contactMappingConfig(mappingBuilder); + return mappingBuilder.Build(); + }); + + services.TryAddSingleton(); + services.TryAddSingleton(); + + return services; + } + /// /// Adds custom service for BizForm item validation before sending to CRM /// diff --git a/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs index 88d45f1..76d02f4 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs @@ -1,4 +1,5 @@ -using CMS.OnlineForms; +using CMS.ContactManagement; +using CMS.OnlineForms; namespace Kentico.Xperience.CRM.Common.Services; @@ -13,6 +14,13 @@ public interface ILeadsIntegrationService /// /// Task CreateLeadAsync(BizFormItem bizFormItem); + + /// + /// Creates lead in CRM from Contact info + /// + /// + /// + Task CreateLeadAsync(ContactInfo contactInfo); /// /// Updates lead in CRM from BizForm item @@ -20,4 +28,11 @@ public interface ILeadsIntegrationService /// /// Task UpdateLeadAsync(BizFormItem bizFormItem); + + /// + /// Updated contact in CRM from BizForm item + /// + /// + /// + Task UpdateLeadAsync(ContactInfo bizFormItem); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormFieldsMappingBuilderExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormFieldsMappingBuilderExtensions.cs index 8b3ba47..e912b21 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormFieldsMappingBuilderExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormFieldsMappingBuilderExtensions.cs @@ -1,8 +1,8 @@ using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Dynamics.Helpers; using Microsoft.Xrm.Sdk; using System.Linq.Expressions; -using System.Reflection; namespace Kentico.Xperience.CRM.Dynamics.Configuration { @@ -26,7 +26,7 @@ public static BizFormFieldsMappingBuilder MapField( Expression> expression) where TLeadEntity : Entity { - string crmFieldName = GetLogicalNameFromExpression(expression); + string crmFieldName = EntityHelper.GetLogicalNameFromExpression(expression); if (crmFieldName == string.Empty) { throw new InvalidOperationException("Attribute name cannot be empty"); @@ -53,7 +53,7 @@ public static BizFormFieldsMappingBuilder MapField( where TBizFormItem : BizFormItem where TLeadEntity : Entity { - string crmFieldName = GetLogicalNameFromExpression(expression); + string crmFieldName = EntityHelper.GetLogicalNameFromExpression(expression); if (crmFieldName == string.Empty) { @@ -62,24 +62,5 @@ public static BizFormFieldsMappingBuilder MapField( return builder.MapField(mappingFunc, crmFieldName); } - - /// - /// Method name is returned from - /// - /// - /// - /// - /// - private static string GetLogicalNameFromExpression( - Expression> expression) - { - if (expression.Body is MemberExpression memberExpression) - { - PropertyInfo propertyInfo = (PropertyInfo)memberExpression.Member; - return propertyInfo.GetCustomAttribute()?.LogicalName ?? string.Empty; - } - - return string.Empty; - } } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs new file mode 100644 index 0000000..104a4b7 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs @@ -0,0 +1,85 @@ +using CMS.ContactManagement; +using CMS.Globalization; +using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; +using Kentico.Xperience.CRM.Dynamics.Helpers; +using Microsoft.Xrm.Sdk; +using System.Linq.Expressions; + +namespace Kentico.Xperience.CRM.Dynamics.Configuration; + +public static class ContactMappingBuilderExtensions +{ + public static ContactMappingBuilder MapField(this ContactMappingBuilder builder, + string contactFieldName, + Expression> expression) + where TCRMEntity : Entity + { + string crmFieldName = EntityHelper.GetLogicalNameFromExpression(expression); + if (crmFieldName == string.Empty) + { + throw new InvalidOperationException("Attribute name cannot be empty"); + } + + builder.MapField(contactFieldName, crmFieldName); + return builder; + } + + public static ContactMappingBuilder MapField(this ContactMappingBuilder builder, + Func contactInfoMappingFunc, Expression> expression) + where TCRMEntity : Entity + { + string crmFieldName = EntityHelper.GetLogicalNameFromExpression(expression); + + if (crmFieldName == string.Empty) + { + throw new InvalidOperationException("Attribute name cannot be empty"); + } + + return builder.MapField(contactInfoMappingFunc, crmFieldName); + } + + public static ContactMappingBuilder AddDefaultMappingForLead(this ContactMappingBuilder builder) + { + builder.MapField(c => c.ContactFirstName, l => l.FirstName); + builder.MapField(c => c.ContactMiddleName, l => l.MiddleName); + builder.MapField(c => c.ContactLastName, l => l.LastName); + builder.MapField(c => c.ContactEmail, l => l.EMailAddress1); + builder.MapField(c => c.ContactAddress1, l => l.Address1_Line1); + builder.MapField(c => c.ContactCity, l => l.Address1_City); + builder.MapField(c => c.ContactZIP, l => l.Address1_PostalCode); + builder.MapField( + c => c.ContactCountryID > 0 + ? CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty + : string.Empty, l => l.Address1_Country); + builder.MapField(c => c.ContactJobTitle, l => l.JobTitle); + builder.MapField(c => c.ContactMobilePhone, l => l.MobilePhone); + builder.MapField(c => c.ContactBusinessPhone, l => l.Telephone1); + builder.MapField(c => c.ContactCompanyName, l => l.CompanyName); + builder.MapField(c => c.ContactNotes, l => l.Description); + + return builder; + } + + public static ContactMappingBuilder AddDefaultMappingForContact(this ContactMappingBuilder builder) + { + builder.MapField(c => c.ContactFirstName, l => l.FirstName); + builder.MapField(c => c.ContactMiddleName, l => l.MiddleName); + builder.MapField(c => c.ContactLastName, l => l.LastName); + builder.MapField(c => c.ContactEmail, l => l.EMailAddress1); + builder.MapField(c => c.ContactAddress1, l => l.Address1_Line1); + builder.MapField(c => c.ContactCity, l => l.Address1_City); + builder.MapField(c => c.ContactZIP, l => l.Address1_PostalCode); + builder.MapField( + c => c.ContactCountryID > 0 + ? CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty + : string.Empty, l => l.Address1_Country); + builder.MapField(c => c.ContactJobTitle, l => l.JobTitle); + builder.MapField(c => c.ContactMobilePhone, l => l.MobilePhone); + builder.MapField(c => c.ContactBusinessPhone, l => l.Telephone1); + builder.MapField(c => c.ContactCompanyName, l => l.Company); + builder.MapField(c => c.ContactNotes, l => l.Description); + + return builder; + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingConfiguration.cs new file mode 100644 index 0000000..59391b4 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingConfiguration.cs @@ -0,0 +1,7 @@ +using Kentico.Xperience.CRM.Common.Configuration; + +namespace Kentico.Xperience.CRM.Dynamics.Configuration; + +public class DynamicsContactMappingConfiguration : ContactMappingConfiguration +{ +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index 1d2d08a..52dac7d 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -1,9 +1,11 @@ using Kentico.Xperience.CRM.Common; using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.PowerPlatform.Dataverse.Client; @@ -23,14 +25,25 @@ public static IServiceCollection AddDynamicsCrmLeadsIntegration(this IServiceCol Action formsConfig, IConfiguration configuration) { - serviceCollection.AddKenticoCrmCommonIntegration(formsConfig); + serviceCollection.AddKenticoCrmCommonLeadIntegration(formsConfig); serviceCollection.AddOptions().Bind(configuration); - serviceCollection.AddSingleton(GetCrmServiceClient); + serviceCollection.TryAddSingleton(GetCrmServiceClient); serviceCollection.AddScoped(); return serviceCollection; } + public static IServiceCollection AddDynamicCrmContactsToLeadsIntegration(this IServiceCollection serviceCollection, + Action contactsConfig, IConfiguration configuration) + { + serviceCollection.AddKenticoCrmCommonContactIntegration(contactsConfig); + + serviceCollection.AddOptions().Bind(configuration); + serviceCollection.TryAddSingleton(GetCrmServiceClient); + + return serviceCollection; + } + /// /// Create Dataverse API client /// diff --git a/src/Kentico.Xperience.CRM.Dynamics/Helpers/EntityHelper.cs b/src/Kentico.Xperience.CRM.Dynamics/Helpers/EntityHelper.cs new file mode 100644 index 0000000..3769cec --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Helpers/EntityHelper.cs @@ -0,0 +1,27 @@ +using Microsoft.Xrm.Sdk; +using System.Linq.Expressions; +using System.Reflection; + +namespace Kentico.Xperience.CRM.Dynamics.Helpers; + +public static class EntityHelper +{ + /// + /// Method name is returned from + /// + /// + /// + /// + /// + public static string GetLogicalNameFromExpression( + Expression> expression) + { + if (expression.Body is MemberExpression memberExpression) + { + var propertyInfo = (PropertyInfo)memberExpression.Member; + return propertyInfo.GetCustomAttribute()?.LogicalName ?? string.Empty; + } + + return string.Empty; + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index 1e8f54b..37a0762 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -21,7 +21,7 @@ public static IServiceCollection AddSalesForceCrmLeadsIntegration(this IServiceC Action formsConfig, IConfiguration configuration) { - serviceCollection.AddKenticoCrmCommonIntegration(formsConfig); + serviceCollection.AddKenticoCrmCommonLeadIntegration(formsConfig); serviceCollection.AddOptions().Bind(configuration); // default cache for token management From 43aefa63b688f3603e7c22210f6138b7fa2e07fa Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Tue, 2 Jan 2024 19:42:43 +0100 Subject: [PATCH 02/26] contacts wip --- .../Services/IContactsIntegrationService.cs | 34 +++++ .../IContactsIntegrationValidationService.cs | 8 ++ .../Services/IFailedSyncItemService.cs | 10 +- .../Services/ILeadsIntegrationService.cs | 14 -- .../ContactsIntegrationValidationService.cs | 10 ++ .../Implementations/FailedSyncItemService.cs | 8 +- .../LeadsIntegrationServiceCommon.cs | 7 +- .../DynamicsServiceCollectionExtensions.cs | 1 + .../DynamicsContactsIntegrationService.cs | 135 ++++++++++++++++++ .../IDynamicsContactsIntegrationService.cs | 7 + 10 files changed, 215 insertions(+), 19 deletions(-) create mode 100644 src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationService.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationValidationService.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Services/Implementations/ContactsIntegrationValidationService.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Services/IDynamicsContactsIntegrationService.cs diff --git a/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationService.cs new file mode 100644 index 0000000..3441798 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationService.cs @@ -0,0 +1,34 @@ +using CMS.ContactManagement; + +namespace Kentico.Xperience.CRM.Common.Services; + +public interface IContactsIntegrationService +{ + /// + /// Creates lead in CRM from Contact info + /// + /// + /// + Task CreateLeadAsync(ContactInfo contactInfo); + + /// + /// Updated contact in CRM from Contact info + /// + /// + /// + Task UpdateLeadAsync(ContactInfo contactInfo); + + /// + /// + /// + /// + /// + Task CreateContactAsync(ContactInfo contactInfo); + + /// + /// + /// + /// + /// + Task UpdateContactAsync(ContactInfo contactInfo); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationValidationService.cs b/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationValidationService.cs new file mode 100644 index 0000000..6fd85d2 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationValidationService.cs @@ -0,0 +1,8 @@ +using CMS.ContactManagement; + +namespace Kentico.Xperience.CRM.Common.Services; + +public interface IContactsIntegrationValidationService +{ + Task ValidateContactInfo(ContactInfo contactInfo); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/IFailedSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/IFailedSyncItemService.cs index 8bd7767..03b10d8 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/IFailedSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/IFailedSyncItemService.cs @@ -1,4 +1,5 @@ -using CMS.OnlineForms; +using CMS.ContactManagement; +using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Classes; namespace Kentico.Xperience.CRM.Common.Services; @@ -15,6 +16,13 @@ public interface IFailedSyncItemService /// BizForm item /// CRM name void LogFailedLeadItem(BizFormItem bizFormItem, string crmName); + + /// + /// @TODO + /// + /// + /// + void LogFailedContactItem(ContactInfo contactInfo, string crmName); /// /// Get all items waiting for synchronization which can be already synced again (according SyncNextTime property) diff --git a/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs index 76d02f4..111cec9 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs @@ -14,13 +14,6 @@ public interface ILeadsIntegrationService /// /// Task CreateLeadAsync(BizFormItem bizFormItem); - - /// - /// Creates lead in CRM from Contact info - /// - /// - /// - Task CreateLeadAsync(ContactInfo contactInfo); /// /// Updates lead in CRM from BizForm item @@ -28,11 +21,4 @@ public interface ILeadsIntegrationService /// /// Task UpdateLeadAsync(BizFormItem bizFormItem); - - /// - /// Updated contact in CRM from BizForm item - /// - /// - /// - Task UpdateLeadAsync(ContactInfo bizFormItem); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/ContactsIntegrationValidationService.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/ContactsIntegrationValidationService.cs new file mode 100644 index 0000000..9dd6623 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/ContactsIntegrationValidationService.cs @@ -0,0 +1,10 @@ +using CMS.ContactManagement; +using CMS.Helpers; + +namespace Kentico.Xperience.CRM.Common.Services.Implementations; + +internal class ContactsIntegrationValidationService : IContactsIntegrationValidationService +{ + public Task ValidateContactInfo(ContactInfo contactInfo) + => Task.FromResult(ValidationHelper.IsEmail(contactInfo.ContactEmail)); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/FailedSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/FailedSyncItemService.cs index c6e1eac..98d174d 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/Implementations/FailedSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/FailedSyncItemService.cs @@ -1,4 +1,5 @@ -using CMS.DataEngine; +using CMS.ContactManagement; +using CMS.DataEngine; using CMS.FormEngine; using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Classes; @@ -49,6 +50,11 @@ public void LogFailedLeadItem(BizFormItem bizFormItem, string crmName) failedSyncItemInfoProvider.Set(existingItem); } + public void LogFailedContactItem(ContactInfo contactInfo, string crmName) + { + throw new NotImplementedException(); + } + public IEnumerable GetFailedSyncItemsToReSync(string crmName) { return failedSyncItemInfoProvider.Get() diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs index 2f21ccc..a484867 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs @@ -1,4 +1,5 @@ -using CMS.OnlineForms; +using CMS.ContactManagement; +using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Configuration; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Microsoft.Extensions.Logging; @@ -66,10 +67,10 @@ public async Task UpdateLeadAsync(BizFormItem bizFormItem) await UpdateLeadAsync(bizFormItem, formMapping); } } - + protected abstract Task CreateLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings); protected abstract Task UpdateLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings); - + protected virtual string FormatExternalId(BizFormItem bizFormItem) => $"{bizFormItem.BizFormClassName}-{bizFormItem.ItemID}"; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index 52dac7d..8aeef62 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -40,6 +40,7 @@ public static IServiceCollection AddDynamicCrmContactsToLeadsIntegration(this IS serviceCollection.AddOptions().Bind(configuration); serviceCollection.TryAddSingleton(GetCrmServiceClient); + serviceCollection.AddScoped(); return serviceCollection; } diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs new file mode 100644 index 0000000..fa32557 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs @@ -0,0 +1,135 @@ +using CMS.ContactManagement; +using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Mapping.Implementations; +using Kentico.Xperience.CRM.Common.Services; +using Kentico.Xperience.CRM.Dynamics.Configuration; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; +using Microsoft.Extensions.Logging; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using System.ServiceModel; + +namespace Kentico.Xperience.CRM.Dynamics.Services; + +public class DynamicsContactsIntegrationService : IDynamicsContactsIntegrationService +{ + private readonly DynamicsContactMappingConfiguration contactMapping; + private readonly IContactsIntegrationValidationService validationService; + private readonly ServiceClient serviceClient; + private readonly ILogger logger; + private readonly IFailedSyncItemService failedSyncItemService; + + public DynamicsContactsIntegrationService(DynamicsContactMappingConfiguration contactMapping, + IContactsIntegrationValidationService validationService, + ServiceClient serviceClient, + ILogger logger, + IFailedSyncItemService failedSyncItemService) + { + this.contactMapping = contactMapping; + this.validationService = validationService; + this.serviceClient = serviceClient; + this.logger = logger; + this.failedSyncItemService = failedSyncItemService; + } + + public async Task CreateLeadAsync(ContactInfo contactInfo) + { + try + { + if (!await validationService.ValidateContactInfo(contactInfo)) + { + logger.LogInformation("Contact info {ItemID} refused by validation", + contactInfo.ContactID); + return; + } + var leadEntity = new Lead(); + MapCRMEntity(contactInfo, leadEntity, contactMapping.FieldsMapping); + + leadEntity.Subject ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; + + await serviceClient.CreateAsync(leadEntity); + } + catch (FaultException e) + { + logger.LogError(e, "Create entity failed - api error: {ApiResult}", e.Detail); + failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); + } + catch (Exception e) when (e.InnerException is FaultException ie) + { + logger.LogError(e, "Create entity failed - api error: {ApiResult}", ie.Detail); + failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); + } + catch (Exception e) + { + logger.LogError(e, "Create entity failed - unknown api error"); + failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); + } + } + + public Task UpdateLeadAsync(ContactInfo contactInfo) + { + throw new NotImplementedException(); + } + + public Task CreateContactAsync(ContactInfo contactInfo) + { + throw new NotImplementedException(); + } + + public Task UpdateContactAsync(ContactInfo contactInfo) + { + throw new NotImplementedException(); + } + + private async Task CreateEntity(ContactInfo contactInfo, string entityType) + { + try + { + if (!await validationService.ValidateContactInfo(contactInfo)) + { + logger.LogInformation("Contact info {ItemID} refused by validation", + contactInfo.ContactID); + return; + } + var leadEntity = new Entity(entityType); + MapCRMEntity(contactInfo, leadEntity, contactMapping.FieldsMapping); + if (entityType == Lead.EntityLogicalName) + { + leadEntity["subject"] ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; + } + + await serviceClient.CreateAsync(leadEntity); + } + catch (FaultException e) + { + logger.LogError(e, "Create entity failed - api error: {ApiResult}", e.Detail); + failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); + } + catch (Exception e) when (e.InnerException is FaultException ie) + { + logger.LogError(e, "Create entity failed - api error: {ApiResult}", ie.Detail); + failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); + } + catch (Exception e) + { + logger.LogError(e, "Create entity failed - unknown api error"); + failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); + } + } + + protected virtual void MapCRMEntity(ContactInfo contactInfo, Entity leadEntity, + IEnumerable fieldMappings) + { + foreach (var fieldMapping in fieldMappings) + { + var formFieldValue = fieldMapping.ContactFieldMapping.MapContactField(contactInfo); + + _ = fieldMapping.CRMFieldMapping switch + { + CRMFieldNameMapping m => leadEntity[m.CrmFieldName] = formFieldValue, + _ => throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), + fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping") + }; + } + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/IDynamicsContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/IDynamicsContactsIntegrationService.cs new file mode 100644 index 0000000..f6dac43 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/IDynamicsContactsIntegrationService.cs @@ -0,0 +1,7 @@ +using Kentico.Xperience.CRM.Common.Services; + +namespace Kentico.Xperience.CRM.Dynamics.Services; + +public interface IDynamicsContactsIntegrationService : IContactsIntegrationService +{ +} \ No newline at end of file From f63943a5048466033898940209e49c5771112b1e Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Wed, 3 Jan 2024 17:46:04 +0100 Subject: [PATCH 03/26] contact sync wip --- examples/DancingGoat/Program.cs | 59 +++++++------- examples/DancingGoat/appsettings.json | 3 +- .../Configuration/ContactMappingBuilder.cs | 2 +- .../ServiceCollectionExtensions.cs | 9 +-- .../Services/IContactsIntegrationService.cs | 18 +---- .../Workers/FailedSyncItemsWorkerBase.cs | 2 +- ....cs => DynamicsIntegrationGlobalEvents.cs} | 37 ++++++--- .../DynamicsServiceCollectionExtensions.cs | 10 ++- .../DynamicsContactsIntegrationService.cs | 76 ++++++++++--------- .../IDynamicsContactsIntegrationService.cs | 3 + .../Workers/ContactsSyncFromCRMWorker.cs | 43 +++++++++++ .../Workers/ContactsSyncQueueWorker.cs | 64 ++++++++++++++++ .../SalesForceBizFormGlobalEvents.cs | 6 +- 13 files changed, 229 insertions(+), 103 deletions(-) rename src/Kentico.Xperience.CRM.Dynamics/{DynamicsBizFormGlobalEvents.cs => DynamicsIntegrationGlobalEvents.cs} (65%) create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncFromCRMWorker.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncQueueWorker.cs diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index 636da6a..6cedcdc 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -51,34 +51,37 @@ ConfigureMembershipServices(builder.Services); //CRM integration registration start -builder.Services.AddDynamicsCrmLeadsIntegration(builder => - builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name - c => c - .MapField("UserFirstName", "firstname") - .MapField("UserLastName", e => e.LastName) //you can map to Lead object or use own generated Lead class - .MapField(c => c.UserEmail, e => e.EMailAddress1) //generated form class used - .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //general BizFormItem used - ) - .ExternalIdField("crf1c_kenticoid") //optional custom field when you want updates to work - , - builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)) //config section with settings - .AddCustomFormLeadsValidationService(); //optional - -builder.Services.AddSalesForceCrmLeadsIntegration(builder => - builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name - c => c - .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names - .MapField("UserLastName", e => e.LastName) //option 2: mapping source name string -> member expression to SObject - .MapField(c => c.UserEmail, e => e.Email) - //option 3: source mapping function from generated BizForm object -> member expression to SObject - .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) - //option 4: source mapping function general BizFormItem -> member expression to SObject - ) - .ExternalIdField("KenticoID__c") //optional custom field when you want updates to work - //.AddForm("formname") // add another forms definitions - , - builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); //config section with settings - +// builder.Services.AddDynamicsCrmLeadsIntegration(builder => +// builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name +// c => c +// .MapField("UserFirstName", "firstname") +// .MapField("UserLastName", e => e.LastName) //you can map to Lead object or use own generated Lead class +// .MapField(c => c.UserEmail, e => e.EMailAddress1) //generated form class used +// .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //general BizFormItem used +// ) +// .ExternalIdField("crf1c_kenticoid") //optional custom field when you want updates to work +// , +// builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)) //config section with settings +// .AddCustomFormLeadsValidationService(); //optional +// +// builder.Services.AddSalesForceCrmLeadsIntegration(builder => +// builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name +// c => c +// .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names +// .MapField("UserLastName", e => e.LastName) //option 2: mapping source name string -> member expression to SObject +// .MapField(c => c.UserEmail, e => e.Email) +// //option 3: source mapping function from generated BizForm object -> member expression to SObject +// .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) +// //option 4: source mapping function general BizFormItem -> member expression to SObject +// ) +// .ExternalIdField("KenticoID__c") //optional custom field when you want updates to work +// //.AddForm("formname") // add another forms definitions +// , +// builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); //config section with settings + + +builder.Services.AddDynamicsCrmContactsToLeadsIntegration(b => { }, + builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //CRM integration registration end var app = builder.Build(); diff --git a/examples/DancingGoat/appsettings.json b/examples/DancingGoat/appsettings.json index 890fdb9..7b64e3f 100644 --- a/examples/DancingGoat/appsettings.json +++ b/examples/DancingGoat/appsettings.json @@ -25,7 +25,8 @@ }, "CMSHashStringSalt": "", "CMSDynamicsCRMIntegration": { - "FormLeadsEnabled": true + "FormLeadsEnabled": true, + "ContactsEnabled": true //"ApiConfig" add to secrets.json }, "CMSSalesForceCRMIntegration": { diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs index 527b01b..363c711 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs @@ -19,7 +19,7 @@ public ContactMappingBuilder MapField(Func mappingFunc, str return this; } - internal TContactMappingConfiguration Build() + public TContactMappingConfiguration Build() where TContactMappingConfiguration : ContactMappingConfiguration, new() { return new TContactMappingConfiguration diff --git a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs index fa2527b..c0cfaef 100644 --- a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs @@ -43,14 +43,7 @@ public static IServiceCollection AddKenticoCrmCommonContactIntegration contactMappingConfig) where TMappingConfiguration : ContactMappingConfiguration, new() { - services.TryAddSingleton( - _ => - { - var mappingBuilder = new ContactMappingBuilder(); - contactMappingConfig(mappingBuilder); - return mappingBuilder.Build(); - }); - + services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationService.cs index 3441798..661c035 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationService.cs @@ -9,26 +9,12 @@ public interface IContactsIntegrationService /// /// /// - Task CreateLeadAsync(ContactInfo contactInfo); + Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo); - /// - /// Updated contact in CRM from Contact info - /// - /// - /// - Task UpdateLeadAsync(ContactInfo contactInfo); - - /// - /// - /// - /// - /// - Task CreateContactAsync(ContactInfo contactInfo); - /// /// /// /// /// - Task UpdateContactAsync(ContactInfo contactInfo); + Task SynchronizeContactToContactsAsync(ContactInfo contactInfo); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs index 3757fbd..877fd4e 100644 --- a/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs @@ -38,7 +38,7 @@ protected override void Process() try { - var settings = Service.Resolve>().Value; + var settings = Service.Resolve>().CurrentValue; if (!settings.FormLeadsEnabled) return; var failedSyncItemsService = Service.Resolve(); diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsBizFormGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs similarity index 65% rename from src/Kentico.Xperience.CRM.Dynamics/DynamicsBizFormGlobalEvents.cs rename to src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs index 8714d08..6c76be5 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsBizFormGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -1,7 +1,9 @@ using CMS; using CMS.Base; +using CMS.ContactManagement; using CMS.Core; using CMS.DataEngine; +using CMS.Helpers; using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Installers; @@ -14,20 +16,20 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -[assembly: RegisterModule(typeof(DynamicsBizFormGlobalEvents))] +[assembly: RegisterModule(typeof(DynamicsIntegrationGlobalEvents))] namespace Kentico.Xperience.CRM.Dynamics; /// -/// Module with bizformitem event handlers for Dynamics integration +/// Module with BizFormItem and ContactInfo event handlers for Dynamics integration /// -internal class DynamicsBizFormGlobalEvents : Module +internal class DynamicsIntegrationGlobalEvents : Module { - public DynamicsBizFormGlobalEvents() : base(nameof(DynamicsBizFormGlobalEvents)) + public DynamicsIntegrationGlobalEvents() : base(nameof(DynamicsIntegrationGlobalEvents)) { } - private ILogger logger = null!; + private ILogger logger = null!; protected override void OnInit() { @@ -35,9 +37,18 @@ protected override void OnInit() BizFormItemEvents.Insert.After += BizFormInserted; BizFormItemEvents.Update.After += BizFormUpdated; - logger = Service.Resolve>(); + + ContactInfo.TYPEINFO.Events.Insert.After += ContactSync; + ContactInfo.TYPEINFO.Events.Insert.After += ContactSync; + + logger = Service.Resolve>(); Service.Resolve().Install(); - ThreadWorker.Current.EnsureRunningThread(); + RequestEvents.RunEndRequestTasks.Execute += (_, _) => + { + ContactsSyncQueueWorker.Current.EnsureRunningThread(); + ContactsSyncFromCRMWorker.Current.EnsureRunningThread(); + FailedItemsWorker.Current.EnsureRunningThread(); + }; } private void BizFormInserted(object? sender, BizFormItemEventArgs e) @@ -45,7 +56,7 @@ private void BizFormInserted(object? sender, BizFormItemEventArgs e) var failedSyncItemsService = Service.Resolve(); try { - var settings = Service.Resolve>().Value; + var settings = Service.Resolve>().CurrentValue; if (!settings.FormLeadsEnabled) return; using (var serviceScope = Service.Resolve().CreateScope()) @@ -67,7 +78,7 @@ private void BizFormUpdated(object? sender, BizFormItemEventArgs e) { try { - var settings = Service.Resolve>().Value; + var settings = Service.Resolve>().CurrentValue; if (!settings.FormLeadsEnabled) return; var mappingConfig = Service.Resolve(); @@ -89,4 +100,12 @@ private void BizFormUpdated(object? sender, BizFormItemEventArgs e) logger.LogError(exception, "Error occured during updating lead"); } } + + private void ContactSync(object? sender, ObjectEventArgs args) + { + if (args.Object is not ContactInfo contactInfo || !ValidationHelper.IsEmail(contactInfo.ContactEmail)) + return; + + ContactsSyncQueueWorker.Current.Enqueue(contactInfo); + } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index 8aeef62..4114408 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -33,10 +33,18 @@ public static IServiceCollection AddDynamicsCrmLeadsIntegration(this IServiceCol return serviceCollection; } - public static IServiceCollection AddDynamicCrmContactsToLeadsIntegration(this IServiceCollection serviceCollection, + public static IServiceCollection AddDynamicsCrmContactsToLeadsIntegration(this IServiceCollection serviceCollection, Action contactsConfig, IConfiguration configuration) { serviceCollection.AddKenticoCrmCommonContactIntegration(contactsConfig); + serviceCollection.TryAddSingleton( + _ => + { + var mappingBuilder = new ContactMappingBuilder(); + mappingBuilder.AddDefaultMappingForLead(); + contactsConfig(mappingBuilder); + return mappingBuilder.Build(); + }); serviceCollection.AddOptions().Bind(configuration); serviceCollection.TryAddSingleton(GetCrmServiceClient); diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs index fa32557..aeee42c 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerPlatform.Dataverse.Client; using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; using System.ServiceModel; namespace Kentico.Xperience.CRM.Dynamics.Services; @@ -32,7 +33,7 @@ public DynamicsContactsIntegrationService(DynamicsContactMappingConfiguration co this.failedSyncItemService = failedSyncItemService; } - public async Task CreateLeadAsync(ContactInfo contactInfo) + public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) { try { @@ -51,37 +52,22 @@ public async Task CreateLeadAsync(ContactInfo contactInfo) } catch (FaultException e) { - logger.LogError(e, "Create entity failed - api error: {ApiResult}", e.Detail); + logger.LogError(e, "Contact/Lead sync failed - api error: {ApiResult}", e.Detail); failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); } catch (Exception e) when (e.InnerException is FaultException ie) { - logger.LogError(e, "Create entity failed - api error: {ApiResult}", ie.Detail); + logger.LogError(e, "Contact/Lead sync failed - api error: {ApiResult}", ie.Detail); failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); } catch (Exception e) { - logger.LogError(e, "Create entity failed - unknown api error"); + logger.LogError(e, "Contact/Lead sync failed - unknown api error"); failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); } } - - public Task UpdateLeadAsync(ContactInfo contactInfo) - { - throw new NotImplementedException(); - } - - public Task CreateContactAsync(ContactInfo contactInfo) - { - throw new NotImplementedException(); - } - - public Task UpdateContactAsync(ContactInfo contactInfo) - { - throw new NotImplementedException(); - } - - private async Task CreateEntity(ContactInfo contactInfo, string entityType) + + public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) { try { @@ -91,32 +77,30 @@ private async Task CreateEntity(ContactInfo contactInfo, string entityType) contactInfo.ContactID); return; } - var leadEntity = new Entity(entityType); + var leadEntity = new Contact(); MapCRMEntity(contactInfo, leadEntity, contactMapping.FieldsMapping); - if (entityType == Lead.EntityLogicalName) - { - leadEntity["subject"] ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; - } + + //leadEntity.Subject ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; await serviceClient.CreateAsync(leadEntity); } catch (FaultException e) { - logger.LogError(e, "Create entity failed - api error: {ApiResult}", e.Detail); + logger.LogError(e, "Contact sync failed - api error: {ApiResult}", e.Detail); failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); } catch (Exception e) when (e.InnerException is FaultException ie) { - logger.LogError(e, "Create entity failed - api error: {ApiResult}", ie.Detail); + logger.LogError(e, "Contact sync failed - api error: {ApiResult}", ie.Detail); failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); } catch (Exception e) { - logger.LogError(e, "Create entity failed - unknown api error"); + logger.LogError(e, "Contact sync failed - unknown api error"); failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); } } - + protected virtual void MapCRMEntity(ContactInfo contactInfo, Entity leadEntity, IEnumerable fieldMappings) { @@ -124,12 +108,34 @@ protected virtual void MapCRMEntity(ContactInfo contactInfo, Entity leadEntity, { var formFieldValue = fieldMapping.ContactFieldMapping.MapContactField(contactInfo); - _ = fieldMapping.CRMFieldMapping switch + if (fieldMapping.CRMFieldMapping is CRMFieldNameMapping m) + { + leadEntity[m.CrmFieldName] = formFieldValue; + } + else { - CRMFieldNameMapping m => leadEntity[m.CrmFieldName] = formFieldValue, - _ => throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), - fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping") - }; + throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), + fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping"); + } } } + + public async Task> GetModifiedLeadsAsync(DateTime lastSync) + { + return await GetModifiedEntitiesAsync(lastSync, Lead.EntityLogicalName); + } + + public async Task> GetModifiedContactsAsync(DateTime lastSync) + { + return await GetModifiedEntitiesAsync(lastSync, Contact.EntityLogicalName); + } + + private async Task> GetModifiedEntitiesAsync(DateTime lastSync, string entityName) + where TEntity : Entity + { + var query = new QueryExpression(entityName) { ColumnSet = new ColumnSet(true) }; + query.Criteria.AddCondition("modifiedon", ConditionOperator.GreaterThan, lastSync.ToUniversalTime()); + + return (await serviceClient.RetrieveMultipleAsync(query)).Entities.Select(e => e.ToEntity()); + } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/IDynamicsContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/IDynamicsContactsIntegrationService.cs index f6dac43..bca04b5 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/IDynamicsContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/IDynamicsContactsIntegrationService.cs @@ -1,7 +1,10 @@ using Kentico.Xperience.CRM.Common.Services; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; namespace Kentico.Xperience.CRM.Dynamics.Services; public interface IDynamicsContactsIntegrationService : IContactsIntegrationService { + Task> GetModifiedLeadsAsync(DateTime lastSync); + Task> GetModifiedContactsAsync(DateTime lastSync); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncFromCRMWorker.cs b/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncFromCRMWorker.cs new file mode 100644 index 0000000..2dd5e5b --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncFromCRMWorker.cs @@ -0,0 +1,43 @@ +using CMS.Base; +using CMS.Core; +using Kentico.Xperience.CRM.Dynamics.Configuration; +using Kentico.Xperience.CRM.Dynamics.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Diagnostics; + +namespace Kentico.Xperience.CRM.Dynamics.Workers; + +public class ContactsSyncFromCRMWorker : ThreadWorker +{ + private readonly ILogger logger = Service.Resolve>(); + protected override int DefaultInterval => 60000; + + protected override void Process() + { + Debug.WriteLine($"Worker {GetType().FullName} running"); + + var settings = Service.Resolve>().CurrentValue; + if (!settings.ContactsEnabled) return; + + try + { + using (var scope = Service.Resolve().CreateScope()) + { + var contactsIntegrationService = + scope.ServiceProvider.GetRequiredService(); + //@TODO + var contacts = contactsIntegrationService.GetModifiedLeadsAsync(DateTime.UtcNow.AddMinutes(-1)) + .GetAwaiter() + .GetResult(); + } + } + catch (Exception e) + { + logger.LogError(e, "Error occured during contacts sync"); + } + } + + protected override void Finish() { } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncQueueWorker.cs b/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncQueueWorker.cs new file mode 100644 index 0000000..644a165 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncQueueWorker.cs @@ -0,0 +1,64 @@ +using CMS.Base; +using CMS.ContactManagement; +using CMS.Core; +using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Services; +using Kentico.Xperience.CRM.Dynamics.Configuration; +using Kentico.Xperience.CRM.Dynamics.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Diagnostics; + +namespace Kentico.Xperience.CRM.Dynamics.Workers; + +internal class ContactsSyncQueueWorker : ThreadQueueWorker +{ + private readonly ILogger logger = Service.Resolve>(); + + /// + protected override int DefaultInterval => 10000; + + /// + protected override void ProcessItem(ContactInfo item) + { + } + + /// + protected override int ProcessItems(IEnumerable contacts) + { + Debug.WriteLine($"Worker {GetType().FullName} running"); + var failedSyncItemsService = Service.Resolve(); + int processed = 0; + + var settings = Service.Resolve>().CurrentValue; + if (!settings.ContactsEnabled) return 0; + + try + { + using (var serviceScope = Service.Resolve().CreateScope()) + { + var contactsIntegrationService = serviceScope.ServiceProvider + .GetRequiredService(); + + foreach (var contact in contacts) + { + contactsIntegrationService.SynchronizeContactToLeadsAsync(contact).ConfigureAwait(false) + .GetAwaiter().GetResult(); + processed++; + } + } + } + catch (Exception exception) + { + logger.LogError(exception, "Error occured during contacts sync"); + //@TODO + //failedSyncItemsService.LogFailedContactItem(contactInfo, CRMType.Dynamics); + } + + return processed; + } + + /// + protected override void Finish() => RunProcess(); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceBizFormGlobalEvents.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceBizFormGlobalEvents.cs index 86b9ec6..5dc88a2 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceBizFormGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceBizFormGlobalEvents.cs @@ -37,7 +37,7 @@ protected override void OnInit() BizFormItemEvents.Update.After += BizFormUpdated; logger = Service.Resolve>(); Service.Resolve().Install(); - ThreadWorker.Current.EnsureRunningThread(); + RequestEvents.RunEndRequestTasks.Execute += (_, _) => FailedItemsWorker.Current.EnsureRunningThread(); } private void BizFormInserted(object? sender, BizFormItemEventArgs e) @@ -45,7 +45,7 @@ private void BizFormInserted(object? sender, BizFormItemEventArgs e) var failedSyncItemsService = Service.Resolve(); try { - var settings = Service.Resolve>().Value; + var settings = Service.Resolve>().CurrentValue; if (!settings.FormLeadsEnabled) return; using (var serviceScope = Service.Resolve().CreateScope()) @@ -67,7 +67,7 @@ private void BizFormUpdated(object? sender, BizFormItemEventArgs e) { try { - var settings = Service.Resolve>().Value; + var settings = Service.Resolve>().CurrentValue; if (!settings.FormLeadsEnabled) return; var mappingConfig = Service.Resolve(); From 5a4029a8caa302a920173ae378d48a42b0b0e52a Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Fri, 5 Jan 2024 19:08:12 +0100 Subject: [PATCH 04/26] contacts sync wip --- examples/DancingGoat/Program.cs | 74 ++++++++------ examples/DancingGoat/appsettings.json | 7 +- .../Configuration/BizFormsMappingBuilder.cs | 18 +++- .../CommonIntegrationSettings.cs | 8 +- .../Configuration/ContactMappingBuilder.cs | 6 ++ .../Enums/ContactCRMType.cs | 7 ++ .../CRMFieldMappingFunction.cs | 4 +- .../Workers/ContactsSyncQueueWorkerBase.cs | 76 +++++++++++++++ .../ContactMappingBuilderExtensions.cs | 2 +- .../DynamicsIntegrationGlobalEvents.cs | 2 +- .../DynamicsServiceCollectionExtensions.cs | 35 ++++--- .../DynamicsContactsIntegrationService.cs | 6 +- .../Workers/ContactsSyncQueueWorker.cs | 53 +--------- .../BizFormFieldsMappingBuilderExtensions.cs | 4 +- .../ContactMappingBuilderExtensions.cs | 80 +++++++++++++++ .../SalesForceContactMappingConfiguration.cs | 7 ++ .../SalesForceBizFormGlobalEvents.cs | 21 +++- .../SalesForceServiceCollectionsExtensions.cs | 66 ++++++++++--- .../ISalesForceContactsIntegrationService.cs | 10 ++ .../SalesForceContactsIntegrationService.cs | 97 +++++++++++++++++++ .../SalesForceLeadsIntegrationService.cs | 1 + .../Workers/ContactsSyncQueueWorker.cs | 12 +++ 22 files changed, 477 insertions(+), 119 deletions(-) create mode 100644 src/Kentico.Xperience.CRM.Common/Enums/ContactCRMType.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Workers/ContactsSyncQueueWorkerBase.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactMappingBuilderExtensions.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceContactMappingConfiguration.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceContactsIntegrationService.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Workers/ContactsSyncQueueWorker.cs diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index 6cedcdc..c9ef1ae 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -10,6 +10,7 @@ using Kentico.PageBuilder.Web.Mvc; using Kentico.Web.Mvc; using Kentico.Xperience.CRM.Common; +using Kentico.Xperience.CRM.Common.Enums; using Kentico.Xperience.CRM.Dynamics; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; @@ -50,38 +51,49 @@ ConfigureMembershipServices(builder.Services); -//CRM integration registration start -// builder.Services.AddDynamicsCrmLeadsIntegration(builder => -// builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name -// c => c -// .MapField("UserFirstName", "firstname") -// .MapField("UserLastName", e => e.LastName) //you can map to Lead object or use own generated Lead class -// .MapField(c => c.UserEmail, e => e.EMailAddress1) //generated form class used -// .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //general BizFormItem used -// ) -// .ExternalIdField("crf1c_kenticoid") //optional custom field when you want updates to work -// , -// builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)) //config section with settings -// .AddCustomFormLeadsValidationService(); //optional -// -// builder.Services.AddSalesForceCrmLeadsIntegration(builder => -// builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name -// c => c -// .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names -// .MapField("UserLastName", e => e.LastName) //option 2: mapping source name string -> member expression to SObject -// .MapField(c => c.UserEmail, e => e.Email) -// //option 3: source mapping function from generated BizForm object -> member expression to SObject -// .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) -// //option 4: source mapping function general BizFormItem -> member expression to SObject -// ) -// .ExternalIdField("KenticoID__c") //optional custom field when you want updates to work -// //.AddForm("formname") // add another forms definitions -// , -// builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); //config section with settings - - -builder.Services.AddDynamicsCrmContactsToLeadsIntegration(b => { }, +// //CRM integration registration start +builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name + c => c + .MapField("UserFirstName", "firstname") + .MapField("UserLastName", e => e.LastName) //you can map to Lead object or use own generated Lead class + .MapField(c => c.UserEmail, e => e.EMailAddress1) //generated form class used + .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //general BizFormItem used + ) + .ExternalIdField("crf1c_kenticoid") //optional custom field when you want updates to work + , + builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)) //config section with settings + .AddCustomFormLeadsValidationService(); //optional + +//With auto mapping (Form contact mapping is used) +builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME), + builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //config section with settings + +builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name + c => c + .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names + .MapField("UserLastName", e => e.LastName) //option 2: mapping source name string -> member expression to SObject + .MapField(c => c.UserEmail, e => e.Email) + //option 3: source mapping function from generated BizForm object -> member expression to SObject + .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) + //option 4: source mapping function general BizFormItem -> member expression to SObject + ) + .ExternalIdField("KenticoID__c") //optional custom field when you want updates to work + //.AddForm("formname") // add another forms definitions + , + builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); //config section with settings + + +// builder.Services.AddDynamicsContactsIntegration(ContactCRMType.Lead, +// builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); + +builder.Services.AddDynamicsContactsIntegration(ContactCRMType.Contact, builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); + +builder.Services.AddSalesForceContactsIntegration(ContactCRMType.Lead, + builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); //CRM integration registration end var app = builder.Build(); diff --git a/examples/DancingGoat/appsettings.json b/examples/DancingGoat/appsettings.json index 7b64e3f..7c66927 100644 --- a/examples/DancingGoat/appsettings.json +++ b/examples/DancingGoat/appsettings.json @@ -25,12 +25,13 @@ }, "CMSHashStringSalt": "", "CMSDynamicsCRMIntegration": { - "FormLeadsEnabled": true, - "ContactsEnabled": true + "FormLeadsEnabled": false, + "ContactsEnabled": false //"ApiConfig" add to secrets.json }, "CMSSalesForceCRMIntegration": { - "FormLeadsEnabled": true + "FormLeadsEnabled": false, + "ContactsEnabled": true //"ApiConfig" add to secrets.json } } diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs index 13001cb..6728dce 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs @@ -8,8 +8,24 @@ public class BizFormsMappingBuilder private readonly Dictionary forms = new(); private string? externalIdFieldName; + /// + /// + /// + /// + /// + /// + public BizFormsMappingBuilder AddFormWithContactMapping(string formCodeName) + { + if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); + //@TODO use Form contact mapping + var mappingBuilder = new BizFormFieldsMappingBuilder(); + + forms.Add(formCodeName.ToLowerInvariant(), mappingBuilder); + return this; + } + public BizFormsMappingBuilder AddForm(string formCodeName, - Func configureFields) + Func configureFields, bool useFormContactMapping = false) { if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs index d8decab..2dfd022 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs @@ -1,4 +1,7 @@ -namespace Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Enums; + +namespace Kentico.Xperience.CRM.Common.Configuration; /// /// Common setting for Kentico-CRM integration @@ -7,8 +10,9 @@ public class CommonIntegrationSettings { public bool FormLeadsEnabled { get; set; } - // @TODO phase 2 public bool ContactsEnabled { get; set; } + + public ContactCRMType ContactType { get; set; } public TApiConfig? ApiConfig { get; set; } } diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs index 363c711..aefb6eb 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs @@ -19,6 +19,12 @@ public ContactMappingBuilder MapField(Func mappingFunc, str return this; } + public ContactMappingBuilder AddMapping(ContactFieldToCRMMapping mapping) + { + fieldMappings.Add(mapping); + return this; + } + public TContactMappingConfiguration Build() where TContactMappingConfiguration : ContactMappingConfiguration, new() { diff --git a/src/Kentico.Xperience.CRM.Common/Enums/ContactCRMType.cs b/src/Kentico.Xperience.CRM.Common/Enums/ContactCRMType.cs new file mode 100644 index 0000000..e914ea7 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Enums/ContactCRMType.cs @@ -0,0 +1,7 @@ +namespace Kentico.Xperience.CRM.Common.Enums; + +public enum ContactCRMType +{ + Lead = 1, + Contact +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/CRMFieldMappingFunction.cs b/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/CRMFieldMappingFunction.cs index 592263a..a64a300 100644 --- a/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/CRMFieldMappingFunction.cs +++ b/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/CRMFieldMappingFunction.cs @@ -9,9 +9,9 @@ namespace Kentico.Xperience.CRM.Common.Mapping.Implementations; /// public class CRMFieldMappingFunction : ICRMFieldMapping { - private readonly Expression> mappingFunc; + private readonly Expression> mappingFunc; - public CRMFieldMappingFunction(Expression> mappingFunc) + public CRMFieldMappingFunction(Expression> mappingFunc) { this.mappingFunc = mappingFunc; } diff --git a/src/Kentico.Xperience.CRM.Common/Workers/ContactsSyncQueueWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Workers/ContactsSyncQueueWorkerBase.cs new file mode 100644 index 0000000..1e08259 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Workers/ContactsSyncQueueWorkerBase.cs @@ -0,0 +1,76 @@ +using CMS.Base; +using CMS.ContactManagement; +using CMS.Core; +using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Enums; +using Kentico.Xperience.CRM.Common.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Diagnostics; + +namespace Kentico.Xperience.CRM.Common.Workers; + +public abstract class ContactsSyncQueueWorkerBase : ThreadQueueWorker + where TWorker : ThreadQueueWorker, new() + where TService : IContactsIntegrationService + where TSettings : CommonIntegrationSettings +{ + private readonly ILogger logger = Service.Resolve>(); + + /// + protected override int DefaultInterval => 10000; + + /// + protected override void ProcessItem(ContactInfo item) + { + } + + /// + protected override int ProcessItems(IEnumerable contacts) + { + Debug.WriteLine($"Worker {GetType().FullName} running"); + var failedSyncItemsService = Service.Resolve(); + int processed = 0; + var contactList = contacts.ToList(); + var settings = Service.Resolve>().CurrentValue; + if (!settings.ContactsEnabled || !contactList.Any()) return 0; + + try + { + using (var serviceScope = Service.Resolve().CreateScope()) + { + var contactsIntegrationService = serviceScope.ServiceProvider + .GetRequiredService(); + + foreach (var contact in contactList) + { + if (settings.ContactType == ContactCRMType.Lead) + { + contactsIntegrationService.SynchronizeContactToLeadsAsync(contact).ConfigureAwait(false) + .GetAwaiter().GetResult(); + } + else + { + contactsIntegrationService.SynchronizeContactToContactsAsync(contact).ConfigureAwait(false) + .GetAwaiter().GetResult(); + } + processed++; + } + } + } + catch (Exception exception) + { + logger.LogError(exception, "Error occured during contacts sync"); + //@TODO + //failedSyncItemsService.LogFailedContactItem(contactInfo, CRMName); + } + + return processed; + } + + /// + protected override void Finish() => RunProcess(); + + protected abstract string CRMName { get; } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs index 104a4b7..a126186 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs @@ -1,6 +1,7 @@ using CMS.ContactManagement; using CMS.Globalization; using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; using Kentico.Xperience.CRM.Dynamics.Helpers; using Microsoft.Xrm.Sdk; @@ -77,7 +78,6 @@ public static ContactMappingBuilder AddDefaultMappingForContact(this ContactMapp builder.MapField(c => c.ContactJobTitle, l => l.JobTitle); builder.MapField(c => c.ContactMobilePhone, l => l.MobilePhone); builder.MapField(c => c.ContactBusinessPhone, l => l.Telephone1); - builder.MapField(c => c.ContactCompanyName, l => l.Company); builder.MapField(c => c.ContactNotes, l => l.Description); return builder; diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs index 6c76be5..db55d36 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -39,7 +39,7 @@ protected override void OnInit() BizFormItemEvents.Update.After += BizFormUpdated; ContactInfo.TYPEINFO.Events.Insert.After += ContactSync; - ContactInfo.TYPEINFO.Events.Insert.After += ContactSync; + ContactInfo.TYPEINFO.Events.Update.After += ContactSync; logger = Service.Resolve>(); Service.Resolve().Install(); diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index 4114408..6db5475 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Kentico.Xperience.CRM.Common; using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Enums; using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Services; @@ -21,7 +22,7 @@ public static class DynamicsServiceCollectionExtensions /// /// /// - public static IServiceCollection AddDynamicsCrmLeadsIntegration(this IServiceCollection serviceCollection, + public static IServiceCollection AddDynamicsFormLeadsIntegration(this IServiceCollection serviceCollection, Action formsConfig, IConfiguration configuration) { @@ -33,23 +34,35 @@ public static IServiceCollection AddDynamicsCrmLeadsIntegration(this IServiceCol return serviceCollection; } - public static IServiceCollection AddDynamicsCrmContactsToLeadsIntegration(this IServiceCollection serviceCollection, - Action contactsConfig, IConfiguration configuration) + public static IServiceCollection AddDynamicsContactsIntegration(this IServiceCollection serviceCollection, + ContactCRMType crmType, IConfiguration configuration) + => serviceCollection.AddDynamicsContactsIntegration(crmType, b => { }, configuration); + + public static IServiceCollection AddDynamicsContactsIntegration(this IServiceCollection serviceCollection, + ContactCRMType crmType, Action mappingConfig, IConfiguration configuration, + bool useDefaultMapping = true) { - serviceCollection.AddKenticoCrmCommonContactIntegration(contactsConfig); + serviceCollection.AddKenticoCrmCommonContactIntegration(mappingConfig); serviceCollection.TryAddSingleton( - _ => + sp => { var mappingBuilder = new ContactMappingBuilder(); - mappingBuilder.AddDefaultMappingForLead(); - contactsConfig(mappingBuilder); + if (useDefaultMapping) + { + mappingBuilder = crmType == ContactCRMType.Lead ? + mappingBuilder.AddDefaultMappingForLead() : + mappingBuilder.AddDefaultMappingForContact(); + mappingConfig(mappingBuilder); + } + return mappingBuilder.Build(); }); - serviceCollection.AddOptions().Bind(configuration); + serviceCollection.AddOptions().Bind(configuration) + .PostConfigure(s => s.ContactType = crmType); serviceCollection.TryAddSingleton(GetCrmServiceClient); serviceCollection.AddScoped(); - + return serviceCollection; } @@ -62,13 +75,13 @@ public static IServiceCollection AddDynamicsCrmContactsToLeadsIntegration(this I private static ServiceClient GetCrmServiceClient(IServiceProvider serviceProvider) { var settings = serviceProvider.GetRequiredService>().Value; - var logger = serviceProvider.GetRequiredService>(); + var logger = serviceProvider.GetRequiredService>(); if (settings.ApiConfig?.IsValid() is not true) { throw new InvalidOperationException("Missing API setting"); } - + var connectionString = string.IsNullOrWhiteSpace(settings.ApiConfig.ConnectionString) ? $"AuthType=ClientSecret;Url={settings.ApiConfig.DynamicsUrl};ClientId={settings.ApiConfig.ClientId};ClientSecret={settings.ApiConfig.ClientSecret}" : settings.ApiConfig.ConnectionString; diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs index aeee42c..19421c7 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs @@ -52,17 +52,17 @@ public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) } catch (FaultException e) { - logger.LogError(e, "Contact/Lead sync failed - api error: {ApiResult}", e.Detail); + logger.LogError(e, "Lead sync failed - api error: {ApiResult}", e.Detail); failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); } catch (Exception e) when (e.InnerException is FaultException ie) { - logger.LogError(e, "Contact/Lead sync failed - api error: {ApiResult}", ie.Detail); + logger.LogError(e, "Lead sync failed - api error: {ApiResult}", ie.Detail); failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); } catch (Exception e) { - logger.LogError(e, "Contact/Lead sync failed - unknown api error"); + logger.LogError(e, "Lead sync failed - unknown api error"); failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); } } diff --git a/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncQueueWorker.cs b/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncQueueWorker.cs index 644a165..2f2e80b 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncQueueWorker.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncQueueWorker.cs @@ -2,7 +2,9 @@ using CMS.ContactManagement; using CMS.Core; using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Enums; using Kentico.Xperience.CRM.Common.Services; +using Kentico.Xperience.CRM.Common.Workers; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Services; using Microsoft.Extensions.DependencyInjection; @@ -12,53 +14,8 @@ namespace Kentico.Xperience.CRM.Dynamics.Workers; -internal class ContactsSyncQueueWorker : ThreadQueueWorker +internal class ContactsSyncQueueWorker : ContactsSyncQueueWorkerBase { - private readonly ILogger logger = Service.Resolve>(); - - /// - protected override int DefaultInterval => 10000; - - /// - protected override void ProcessItem(ContactInfo item) - { - } - - /// - protected override int ProcessItems(IEnumerable contacts) - { - Debug.WriteLine($"Worker {GetType().FullName} running"); - var failedSyncItemsService = Service.Resolve(); - int processed = 0; - - var settings = Service.Resolve>().CurrentValue; - if (!settings.ContactsEnabled) return 0; - - try - { - using (var serviceScope = Service.Resolve().CreateScope()) - { - var contactsIntegrationService = serviceScope.ServiceProvider - .GetRequiredService(); - - foreach (var contact in contacts) - { - contactsIntegrationService.SynchronizeContactToLeadsAsync(contact).ConfigureAwait(false) - .GetAwaiter().GetResult(); - processed++; - } - } - } - catch (Exception exception) - { - logger.LogError(exception, "Error occured during contacts sync"); - //@TODO - //failedSyncItemsService.LogFailedContactItem(contactInfo, CRMType.Dynamics); - } - - return processed; - } - - /// - protected override void Finish() => RunProcess(); + protected override string CRMName => CRMType.Dynamics; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/BizFormFieldsMappingBuilderExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/BizFormFieldsMappingBuilderExtensions.cs index a2f7995..cd7c714 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Configuration/BizFormFieldsMappingBuilderExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/BizFormFieldsMappingBuilderExtensions.cs @@ -20,7 +20,7 @@ public static class BizFormFieldsMappingBuilderExtensions /// public static BizFormFieldsMappingBuilder MapField( this BizFormFieldsMappingBuilder builder, string formFieldName, - Expression> expression) + Expression> expression) { return builder.AddMapping(new BizFormFieldMapping(new BizFormFieldNameMapping(formFieldName), new CRMFieldMappingFunction(expression))); @@ -36,7 +36,7 @@ public static BizFormFieldsMappingBuilder MapField( /// public static BizFormFieldsMappingBuilder MapField( this BizFormFieldsMappingBuilder builder, Func formMappingFunc, - Expression> crmMappingFunc) + Expression> crmMappingFunc) where TBizFormItem : BizFormItem { return builder.AddMapping(new BizFormFieldMapping( diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactMappingBuilderExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactMappingBuilderExtensions.cs new file mode 100644 index 0000000..cfa7dd8 --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactMappingBuilderExtensions.cs @@ -0,0 +1,80 @@ +using CMS.ContactManagement; +using CMS.Globalization; +using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Mapping.Implementations; +using SalesForce.OpenApi; +using System.Linq.Expressions; + +namespace Kentico.Xperience.CRM.SalesForce.Configuration; + +public static class ContactMappingBuilderExtensions +{ + public static ContactMappingBuilder MapLeadField(this ContactMappingBuilder builder, string contactFieldName, + Expression> expression) + { + return builder.AddMapping(new ContactFieldToCRMMapping(new ContactFieldNameMapping(contactFieldName), + new CRMFieldMappingFunction(expression))); + } + + public static ContactMappingBuilder MapLeadField(this ContactMappingBuilder builder, + Func contactInfoMappingFunc, + Expression> expression) + { + return builder.AddMapping(new ContactFieldToCRMMapping(new ContactFieldMappingFunction(contactInfoMappingFunc), + new CRMFieldMappingFunction(expression))); + } + + public static ContactMappingBuilder MapContactField(this ContactMappingBuilder builder, string contactFieldName, + Expression> expression) + { + return builder.AddMapping(new ContactFieldToCRMMapping(new ContactFieldNameMapping(contactFieldName), + new CRMFieldMappingFunction(expression))); + } + + public static ContactMappingBuilder MapContactField(this ContactMappingBuilder builder, + Func contactInfoMappingFunc, + Expression> expression) + { + return builder.AddMapping(new ContactFieldToCRMMapping(new ContactFieldMappingFunction(contactInfoMappingFunc), + new CRMFieldMappingFunction(expression))); + } + + public static ContactMappingBuilder AddDefaultMappingForLead(this ContactMappingBuilder builder) + { + builder.MapLeadField(c => c.ContactFirstName, l => l.FirstName); + builder.MapLeadField(c => c.ContactLastName, l => l.LastName); + builder.MapLeadField(c => c.ContactEmail, l => l.Email); + builder.MapLeadField(c => c.ContactAddress1, l => l.Street); + builder.MapLeadField(c => c.ContactCity, l => l.City); + builder.MapLeadField(c => c.ContactZIP, l => l.PostalCode); + builder.MapLeadField( + c => c.ContactCountryID > 0 + ? CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty + : string.Empty, l => l.Country); + builder.MapLeadField(c => c.ContactMobilePhone, l => l.MobilePhone); + builder.MapLeadField(c => c.ContactBusinessPhone, l => l.Phone); + builder.MapLeadField(c => c.ContactCompanyName, l => l.Company); + builder.MapLeadField(c => c.ContactNotes, l => l.Description); + + return builder; + } + + public static ContactMappingBuilder AddDefaultMappingForContact(this ContactMappingBuilder builder) + { + builder.MapContactField(c => c.ContactFirstName, l => l.FirstName); + builder.MapContactField(c => c.ContactLastName, l => l.LastName); + builder.MapContactField(c => c.ContactEmail, l => l.Email); + builder.MapContactField(c => c.ContactAddress1, l => l.MailingStreet); + builder.MapContactField(c => c.ContactCity, l => l.MailingCity); + builder.MapContactField(c => c.ContactZIP, l => l.MailingPostalCode); + builder.MapContactField( + c => c.ContactCountryID > 0 + ? CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty + : string.Empty, l => l.MailingCountry); + builder.MapContactField(c => c.ContactMobilePhone, l => l.MobilePhone); + builder.MapContactField(c => c.ContactBusinessPhone, l => l.Phone); + builder.MapContactField(c => c.ContactNotes, l => l.Description); + + return builder; + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceContactMappingConfiguration.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceContactMappingConfiguration.cs new file mode 100644 index 0000000..8391dfc --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceContactMappingConfiguration.cs @@ -0,0 +1,7 @@ +using Kentico.Xperience.CRM.Common.Configuration; + +namespace Kentico.Xperience.CRM.SalesForce.Configuration; + +public class SalesForceContactMappingConfiguration : ContactMappingConfiguration +{ +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceBizFormGlobalEvents.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceBizFormGlobalEvents.cs index 5dc88a2..eb9f42d 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceBizFormGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceBizFormGlobalEvents.cs @@ -1,7 +1,9 @@ using CMS; using CMS.Base; +using CMS.ContactManagement; using CMS.Core; using CMS.DataEngine; +using CMS.Helpers; using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Installers; @@ -35,9 +37,18 @@ protected override void OnInit() BizFormItemEvents.Insert.After += BizFormInserted; BizFormItemEvents.Update.After += BizFormUpdated; + + ContactInfo.TYPEINFO.Events.Insert.After += ContactSync; + ContactInfo.TYPEINFO.Events.Update.After += ContactSync; + logger = Service.Resolve>(); Service.Resolve().Install(); - RequestEvents.RunEndRequestTasks.Execute += (_, _) => FailedItemsWorker.Current.EnsureRunningThread(); + RequestEvents.RunEndRequestTasks.Execute += (_, _) => + { + ContactsSyncQueueWorker.Current.EnsureRunningThread(); + //ContactsSyncFromCRMWorker.Current.EnsureRunningThread(); //@TODO + FailedItemsWorker.Current.EnsureRunningThread(); + }; } private void BizFormInserted(object? sender, BizFormItemEventArgs e) @@ -89,4 +100,12 @@ private void BizFormUpdated(object? sender, BizFormItemEventArgs e) logger.LogError(exception, "Error occured during updating lead"); } } + + private void ContactSync(object? sender, ObjectEventArgs args) + { + if (args.Object is not ContactInfo contactInfo || !ValidationHelper.IsEmail(contactInfo.ContactEmail)) + return; + + ContactsSyncQueueWorker.Current.Enqueue(contactInfo); + } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index 37a0762..4b6c9a3 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -1,9 +1,11 @@ using Kentico.Xperience.CRM.Common; using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Enums; using Kentico.Xperience.CRM.SalesForce.Configuration; using Kentico.Xperience.CRM.SalesForce.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using System.Globalization; namespace Kentico.Xperience.CRM.SalesForce; @@ -17,13 +19,55 @@ public static class SalesForceServiceCollectionsExtensions /// /// /// - public static IServiceCollection AddSalesForceCrmLeadsIntegration(this IServiceCollection serviceCollection, + public static IServiceCollection AddSalesForceFormLeadsIntegration(this IServiceCollection serviceCollection, Action formsConfig, IConfiguration configuration) { serviceCollection.AddKenticoCrmCommonLeadIntegration(formsConfig); + serviceCollection.AddOptions().Bind(configuration); + AddSalesForceCommonIntegration(serviceCollection, configuration); + serviceCollection.AddScoped(); + return serviceCollection; + } + + public static IServiceCollection AddSalesForceContactsIntegration(this IServiceCollection serviceCollection, + ContactCRMType crmType, IConfiguration configuration) + => serviceCollection.AddSalesForceContactsIntegration(crmType, b => { }, configuration); + + public static IServiceCollection AddSalesForceContactsIntegration(this IServiceCollection serviceCollection, + ContactCRMType crmType, + Action mappingConfig, + IConfiguration configuration, + bool useDefaultMapping = true) + { + serviceCollection.AddKenticoCrmCommonContactIntegration(mappingConfig); + serviceCollection.TryAddSingleton( + sp => + { + var mappingBuilder = new ContactMappingBuilder(); + if (useDefaultMapping) + { + mappingBuilder = crmType == ContactCRMType.Lead ? + mappingBuilder.AddDefaultMappingForLead() : + mappingBuilder.AddDefaultMappingForContact(); + mappingConfig(mappingBuilder); + } + + return mappingBuilder.Build(); + }); + + serviceCollection.AddOptions().Bind(configuration) + .PostConfigure(s => s.ContactType = crmType); + AddSalesForceCommonIntegration(serviceCollection, configuration); + + serviceCollection.AddScoped(); + return serviceCollection; + } + + private static void AddSalesForceCommonIntegration(IServiceCollection serviceCollection, IConfiguration configuration) + { // default cache for token management serviceCollection.AddDistributedMemoryCache(); @@ -44,19 +88,15 @@ public static IServiceCollection AddSalesForceCrmLeadsIntegration(this IServiceC //add http client for salesforce api serviceCollection.AddHttpClient(client => - { - var apiConfig = configuration.Get()?.ApiConfig; + { + var apiConfig = configuration.Get()?.ApiConfig; - if (apiConfig?.IsValid() is not true) - throw new InvalidOperationException("Missing API settings"); - - string apiVersion = apiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); - client.BaseAddress = new Uri($"{apiConfig.SalesForceUrl?.TrimEnd('/')}/services/data/v{apiVersion}/"); - }) - .AddClientCredentialsTokenHandler("salesforce.api.client"); - + if (apiConfig?.IsValid() is not true) + throw new InvalidOperationException("Missing API settings"); - serviceCollection.AddScoped(); - return serviceCollection; + string apiVersion = apiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); + client.BaseAddress = new Uri($"{apiConfig.SalesForceUrl?.TrimEnd('/')}/services/data/v{apiVersion}/"); + }) + .AddClientCredentialsTokenHandler("salesforce.api.client"); } } diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceContactsIntegrationService.cs new file mode 100644 index 0000000..a5ba548 --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceContactsIntegrationService.cs @@ -0,0 +1,10 @@ +using Kentico.Xperience.CRM.Common.Services; +using SalesForce.OpenApi; + +namespace Kentico.Xperience.CRM.SalesForce.Services; + +public interface ISalesForceContactsIntegrationService : IContactsIntegrationService +{ + Task> GetModifiedLeadsAsync(DateTime lastSync); + Task> GetModifiedContactsAsync(DateTime lastSync); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs new file mode 100644 index 0000000..e9d5ce7 --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs @@ -0,0 +1,97 @@ +using CMS.ContactManagement; +using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Mapping.Implementations; +using Kentico.Xperience.CRM.Common.Services; +using Kentico.Xperience.CRM.SalesForce.Configuration; +using Microsoft.Extensions.Logging; +using SalesForce.OpenApi; +using System.Text.Json; + +namespace Kentico.Xperience.CRM.SalesForce.Services; + +internal class SalesForceContactsIntegrationService : ISalesForceContactsIntegrationService +{ + private readonly SalesForceContactMappingConfiguration contactMapping; + private readonly IContactsIntegrationValidationService validationService; + private readonly ISalesForceApiService apiService; + private readonly ILogger logger; + private readonly IFailedSyncItemService failedSyncItemService; + + public SalesForceContactsIntegrationService( + SalesForceContactMappingConfiguration contactMapping, + IContactsIntegrationValidationService validationService, + ISalesForceApiService apiService, + ILogger logger, + IFailedSyncItemService failedSyncItemService) + { + this.contactMapping = contactMapping; + this.validationService = validationService; + this.apiService = apiService; + this.logger = logger; + this.failedSyncItemService = failedSyncItemService; + } + + public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) + { + try + { + var lead = new LeadSObject(); + MapLead(contactInfo, lead, contactMapping.FieldsMapping); + + lead.LeadSource ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; + lead.Company ??= "undefined"; //required field - set to 'undefined' to prevent errors + // if (bizFormMappingConfig.ExternalIdFieldName is { Length: > 0 } externalIdFieldName) + // { + // lead.AdditionalProperties[externalIdFieldName] = FormatExternalId(bizFormItem); + // } + + await apiService.CreateLeadAsync(lead); + } + catch (ApiException> e) + { + logger.LogError(e, "Create lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); + //failedSyncItemService.LogFailedLeadItem(contactInfo, CRMType.SalesForce); + } + catch (ApiException> e) + { + logger.LogError(e, "Create lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); + //failedSyncItemService.LogFailedLeadItem(contactInfo, CRMType.SalesForce); + } + catch (ApiException e) + { + logger.LogError(e, "Create lead failed - unexpected api error"); + //failedSyncItemService.LogFailedLeadItem(contactInfo, CRMType.SalesForce); + } + } + + public Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) + { + throw new NotImplementedException(); + } + + public Task> GetModifiedLeadsAsync(DateTime lastSync) + { + throw new NotImplementedException(); + } + + public Task> GetModifiedContactsAsync(DateTime lastSync) + { + throw new NotImplementedException(); + } + + protected virtual void MapLead(ContactInfo contactInfo, LeadSObject lead, + IEnumerable fieldMappings) + { + foreach (var fieldMapping in fieldMappings) + { + var formFieldValue = fieldMapping.ContactFieldMapping.MapContactField(contactInfo); + _ = fieldMapping.CRMFieldMapping switch + { + CRMFieldNameMapping m => lead.AdditionalProperties[m.CrmFieldName] = formFieldValue, + CRMFieldMappingFunction m => m.MapCrmField(lead, formFieldValue), + _ => throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), + fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping") + }; + } + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs index 8392607..8717e01 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs @@ -48,6 +48,7 @@ protected override async Task CreateLeadAsync(BizFormItem bizFormItem, } await apiService.CreateLeadAsync(lead); + return true; } catch (ApiException> e) diff --git a/src/Kentico.Xperience.CRM.SalesForce/Workers/ContactsSyncQueueWorker.cs b/src/Kentico.Xperience.CRM.SalesForce/Workers/ContactsSyncQueueWorker.cs new file mode 100644 index 0000000..099795a --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Workers/ContactsSyncQueueWorker.cs @@ -0,0 +1,12 @@ +using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Workers; +using Kentico.Xperience.CRM.SalesForce.Configuration; +using Kentico.Xperience.CRM.SalesForce.Services; + +namespace Kentico.Xperience.CRM.SalesForce.Workers; + +public class ContactsSyncQueueWorker : ContactsSyncQueueWorkerBase +{ + protected override string CRMName => CRMType.SalesForce; +} \ No newline at end of file From ce68754f79b72d3416afea7523d534a1fe89a4be Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Tue, 16 Jan 2024 13:41:44 +0100 Subject: [PATCH 05/26] cln --- .../Configuration/CommonIntegrationSettings.cs | 3 +-- .../Implementations/ContactFieldNameMapping.cs | 1 - .../ServiceCollectionExtensions.cs | 1 - .../Services/ILeadsIntegrationService.cs | 3 +-- .../Configuration/ContactMappingBuilderExtensions.cs | 1 - .../Workers/ContactsSyncQueueWorker.cs | 11 +---------- .../SalesForceServiceCollectionsExtensions.cs | 2 +- .../Services/SalesForceContactsIntegrationService.cs | 1 - 8 files changed, 4 insertions(+), 19 deletions(-) diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs index 79ef76c..62d0d28 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs @@ -1,5 +1,4 @@ -using Kentico.Xperience.CRM.Common.Constants; -using Kentico.Xperience.CRM.Common.Enums; +using Kentico.Xperience.CRM.Common.Enums; namespace Kentico.Xperience.CRM.Common.Configuration; diff --git a/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldNameMapping.cs b/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldNameMapping.cs index 337447b..e8c5421 100644 --- a/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldNameMapping.cs +++ b/src/Kentico.Xperience.CRM.Common/Mapping/Implementations/ContactFieldNameMapping.cs @@ -1,5 +1,4 @@ using CMS.ContactManagement; -using CMS.OnlineForms; namespace Kentico.Xperience.CRM.Common.Mapping.Implementations; diff --git a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs index aa9bac1..b3c592c 100644 --- a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ using Kentico.Xperience.CRM.Common.Services.Implementations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using System; namespace Kentico.Xperience.CRM.Common; diff --git a/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs index c81e809..a917e4a 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs @@ -1,5 +1,4 @@ -using CMS.ContactManagement; -using CMS.OnlineForms; +using CMS.OnlineForms; namespace Kentico.Xperience.CRM.Common.Services; diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs index a126186..cd35e4f 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs @@ -1,7 +1,6 @@ using CMS.ContactManagement; using CMS.Globalization; using Kentico.Xperience.CRM.Common.Configuration; -using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; using Kentico.Xperience.CRM.Dynamics.Helpers; using Microsoft.Xrm.Sdk; diff --git a/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncQueueWorker.cs b/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncQueueWorker.cs index 2f2e80b..78b9aac 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncQueueWorker.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncQueueWorker.cs @@ -1,16 +1,7 @@ -using CMS.Base; -using CMS.ContactManagement; -using CMS.Core; -using Kentico.Xperience.CRM.Common.Constants; -using Kentico.Xperience.CRM.Common.Enums; -using Kentico.Xperience.CRM.Common.Services; +using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Workers; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Services; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.Diagnostics; namespace Kentico.Xperience.CRM.Dynamics.Workers; diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index a813d43..9516bff 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -2,8 +2,8 @@ using CMS.Helpers; using Duende.AccessTokenManagement; using Kentico.Xperience.CRM.Common; -using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Enums; using Kentico.Xperience.CRM.SalesForce.Configuration; using Kentico.Xperience.CRM.SalesForce.Services; diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs index e9d5ce7..44eec93 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs @@ -1,5 +1,4 @@ using CMS.ContactManagement; -using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.SalesForce.Configuration; From 52bb44feddd6ef6106321c9e3c12fb5a4cdecb2f Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Wed, 17 Jan 2024 16:26:39 +0100 Subject: [PATCH 06/26] wip --- examples/DancingGoat/Program.cs | 2 - examples/DancingGoat/appsettings.json | 4 +- examples/DancingGoat/packages.lock.json | 3 +- .../Configuration/ContactMappingBuilder.cs | 48 +- .../ContactMappingConfiguration.cs | 3 +- .../Constants/SettingKeys.cs | 2 + .../Installers/CRMModuleInstaller.cs | 16 + .../ServiceCollectionExtensions.cs | 16 +- .../ContactMappingBuilderExtensions.cs | 84 --- .../DynamicsContactMappingBuilder.cs | 119 +++ .../DynamicsServiceCollectionExtensions.cs | 48 +- .../ContactMappingBuilderExtensions.cs | 80 -- .../SalesForceContactMappingBuilder.cs | 120 +++ .../Kentico.Xperience.CRM.SalesForce.csproj | 1 + .../SalesForceServiceCollectionsExtensions.cs | 32 +- .../SalesForceContactsIntegrationService.cs | 4 - .../packages.lock.json | 704 +++++++++++++++++- 17 files changed, 1047 insertions(+), 239 deletions(-) delete mode 100644 src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingBuilder.cs delete mode 100644 src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactMappingBuilderExtensions.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceContactMappingBuilder.cs diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index f42caa4..2748bb2 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -1,4 +1,3 @@ -using CMS.OnlineForms; using CMS.OnlineForms.Types; using DancingGoat; using DancingGoat.Models; @@ -9,7 +8,6 @@ using Kentico.OnlineMarketing.Web.Mvc; using Kentico.PageBuilder.Web.Mvc; using Kentico.Web.Mvc; -using Kentico.Xperience.CRM.Common; using Kentico.Xperience.CRM.Common.Enums; using Kentico.Xperience.CRM.Dynamics; using Kentico.Xperience.CRM.Dynamics.Configuration; diff --git a/examples/DancingGoat/appsettings.json b/examples/DancingGoat/appsettings.json index a51a409..0e3d373 100644 --- a/examples/DancingGoat/appsettings.json +++ b/examples/DancingGoat/appsettings.json @@ -26,13 +26,13 @@ "CMSHashStringSalt": "", "CMSDynamicsCRMIntegration": { "FormLeadsEnabled": true, - "ContactsEnabled": false + "ContactsEnabled": false, "IgnoreExistingRecords": false //"ApiConfig" add to secrets.json }, "CMSSalesForceCRMIntegration": { "FormLeadsEnabled": true, - "ContactsEnabled": true + "ContactsEnabled": false, "IgnoreExistingRecords": false //"ApiConfig" add to secrets.json diff --git a/examples/DancingGoat/packages.lock.json b/examples/DancingGoat/packages.lock.json index 55250dc..01fd21d 100644 --- a/examples/DancingGoat/packages.lock.json +++ b/examples/DancingGoat/packages.lock.json @@ -1389,7 +1389,8 @@ "Duende.AccessTokenManagement.OpenIdConnect": "[2.0.3, )", "IdentityModel": "[6.2.0, )", "Kentico.Xperience.CRM.Common": "[1.0.0-prerelease-1, )", - "Kentico.Xperience.Core": "[27.0.1, )" + "Kentico.Xperience.Core": "[27.0.1, )", + "Kentico.Xperience.Dynamics.CRM.Integration": "[1.0.0-prerelease-1, )" } }, "Kentico.Xperience.Dynamics.CRM.Integration": { diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs index aefb6eb..6e0f6cb 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs @@ -1,36 +1,46 @@ using CMS.ContactManagement; using Kentico.Xperience.CRM.Common.Mapping.Implementations; +using Kentico.Xperience.CRM.Common.Services; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.Extensions.DependencyInjection; namespace Kentico.Xperience.CRM.Common.Configuration; -public class ContactMappingBuilder +public abstract class ContactMappingBuilder +where TBuilder : ContactMappingBuilder { - private List fieldMappings = new(); - - public ContactMappingBuilder MapField(string contactFieldName, string crmFieldName) + private readonly IServiceCollection serviceCollection; + protected readonly List fieldMappings = new(); + protected readonly List converters = new(); + + protected ContactMappingBuilder(IServiceCollection serviceCollection) { - fieldMappings.Add(new ContactFieldToCRMMapping(new ContactFieldNameMapping(contactFieldName), new CRMFieldNameMapping(crmFieldName))); - return this; + this.serviceCollection = serviceCollection; } - public ContactMappingBuilder MapField(Func mappingFunc, string crmFieldName) + public TBuilder MapField(string contactFieldName, string crmFieldName) { - fieldMappings.Add(new ContactFieldToCRMMapping(new ContactFieldMappingFunction(mappingFunc), new CRMFieldNameMapping(crmFieldName))); - return this; + fieldMappings.Add(new ContactFieldToCRMMapping(new ContactFieldNameMapping(contactFieldName), new CRMFieldNameMapping(crmFieldName))); + return (TBuilder)this; } - - public ContactMappingBuilder AddMapping(ContactFieldToCRMMapping mapping) + + public TBuilder MapField(Func mappingFunc, string crmFieldName) { - fieldMappings.Add(mapping); - return this; + fieldMappings.Add(new ContactFieldToCRMMapping(new ContactFieldMappingFunction(mappingFunc), new CRMFieldNameMapping(crmFieldName))); + return (TBuilder)this; } - public TContactMappingConfiguration Build() - where TContactMappingConfiguration : ContactMappingConfiguration, new() + /// + /// Adds custom service for BizForm item validation before sending to CRM + /// + /// + /// + /// + public TBuilder AddCustomValidation() + where TService : class, ILeadsIntegrationValidationService { - return new TContactMappingConfiguration - { - FieldsMapping = fieldMappings - }; + serviceCollection.AddSingleton(); + + return (TBuilder)this; } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs index 834680e..d259a9d 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs @@ -4,5 +4,6 @@ namespace Kentico.Xperience.CRM.Common.Configuration; public class ContactMappingConfiguration { - public List FieldsMapping { get; internal init; } = new(); + public List FieldsMapping { get; init; } = new(); + public List Converters { get; init; } = new(); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs b/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs index aa7cd5d..27a9a4e 100644 --- a/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs +++ b/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs @@ -6,12 +6,14 @@ public class SettingKeys { public const string DynamicsFormLeadsEnabled = "CMSDynamicsCRMIntegrationFormLeadsEnabled"; + public const string DynamicsContactsEnabled = "CMSDynamicsCRMIntegrationFormLeadsEnabled"; public const string DynamicsUrl = "CMSDynamicsCRMIntegrationDynamicsUrl"; public const string DynamicsClientId = "CMSDynamicsCRMIntegrationClientId"; public const string DynamicsClientSecret = "CMSDynamicsCRMIntegrationClientSecret"; public const string DynamicsIgnoreExistingRecords = "CMSDynamicsCRMIntegrationIgnoreExistingRecords"; public const string SalesForceFormLeadsEnabled = "CMSSalesforceCRMIntegrationFormLeadsEnabled"; + public const string SalesForceContactsEnabled = "CMSDynamicsCRMIntegrationContactsEnabled"; public const string SalesForceUrl = "CMSSalesforceCRMIntegrationSalesforceUrl"; public const string SalesForceClientId = "CMSSalesforceCRMIntegrationClientId"; public const string SalesForceClientSecret = "CMSSalesforceCRMIntegrationClientSecret"; diff --git a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs index 2a0728b..2d154a9 100644 --- a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs @@ -298,6 +298,22 @@ private void InstallSettings(ResourceInfo resourceInfo, string crmType) SettingsKeyInfo.Provider.Set(settingsIgnoreExisting); } + if (settingFormsEnabled is null) + { + settingFormsEnabled = new SettingsKeyInfo + { + KeyName = $"CMS{crmType}CRMIntegrationContactsEnabled", + KeyDisplayName = "Contact synchronization enabled", + KeyDescription = "", + KeyType = "boolean", + KeyCategoryID = crmCategory.CategoryID, + KeyIsCustom = true, + KeyExplanationText = "", + }; + + SettingsKeyInfo.Provider.Set(settingFormsEnabled); + } + var settingUrl = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegration{crmType}Url"); if (settingUrl is null) { diff --git a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs index b3c592c..39cad74 100644 --- a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs @@ -13,17 +13,15 @@ namespace Kentico.Xperience.CRM.Common; public static class ServiceCollectionExtensions { /// - /// Adds common services for CRM integration. This method is usually used from specific CRM integration library + /// Adds common services for BizForm-Leads CRM integration. This method is usually used from specific CRM integration library /// /// /// /// /// - public static IServiceCollection AddKenticoCrmCommonFormLeadsIntegration( - this IServiceCollection services) + public static IServiceCollection AddKenticoCrmCommonFormLeadsIntegration(this IServiceCollection services) { services.TryAddSingleton(); - services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); @@ -31,13 +29,17 @@ public static IServiceCollection AddKenticoCrmCommonFormLeadsIntegration( return services; } - public static IServiceCollection AddKenticoCrmCommonContactIntegration( - this IServiceCollection services, Action contactMappingConfig) - where TMappingConfiguration : ContactMappingConfiguration, new() + /// + /// Adds common services for Contacts to Leads/Contacts CRM integration. This method is usually used from specific CRM integration library + /// + /// + /// + public static IServiceCollection AddKenticoCrmCommonContactIntegration(this IServiceCollection services) { services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); return services; } diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs deleted file mode 100644 index cd35e4f..0000000 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactMappingBuilderExtensions.cs +++ /dev/null @@ -1,84 +0,0 @@ -using CMS.ContactManagement; -using CMS.Globalization; -using Kentico.Xperience.CRM.Common.Configuration; -using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; -using Kentico.Xperience.CRM.Dynamics.Helpers; -using Microsoft.Xrm.Sdk; -using System.Linq.Expressions; - -namespace Kentico.Xperience.CRM.Dynamics.Configuration; - -public static class ContactMappingBuilderExtensions -{ - public static ContactMappingBuilder MapField(this ContactMappingBuilder builder, - string contactFieldName, - Expression> expression) - where TCRMEntity : Entity - { - string crmFieldName = EntityHelper.GetLogicalNameFromExpression(expression); - if (crmFieldName == string.Empty) - { - throw new InvalidOperationException("Attribute name cannot be empty"); - } - - builder.MapField(contactFieldName, crmFieldName); - return builder; - } - - public static ContactMappingBuilder MapField(this ContactMappingBuilder builder, - Func contactInfoMappingFunc, Expression> expression) - where TCRMEntity : Entity - { - string crmFieldName = EntityHelper.GetLogicalNameFromExpression(expression); - - if (crmFieldName == string.Empty) - { - throw new InvalidOperationException("Attribute name cannot be empty"); - } - - return builder.MapField(contactInfoMappingFunc, crmFieldName); - } - - public static ContactMappingBuilder AddDefaultMappingForLead(this ContactMappingBuilder builder) - { - builder.MapField(c => c.ContactFirstName, l => l.FirstName); - builder.MapField(c => c.ContactMiddleName, l => l.MiddleName); - builder.MapField(c => c.ContactLastName, l => l.LastName); - builder.MapField(c => c.ContactEmail, l => l.EMailAddress1); - builder.MapField(c => c.ContactAddress1, l => l.Address1_Line1); - builder.MapField(c => c.ContactCity, l => l.Address1_City); - builder.MapField(c => c.ContactZIP, l => l.Address1_PostalCode); - builder.MapField( - c => c.ContactCountryID > 0 - ? CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty - : string.Empty, l => l.Address1_Country); - builder.MapField(c => c.ContactJobTitle, l => l.JobTitle); - builder.MapField(c => c.ContactMobilePhone, l => l.MobilePhone); - builder.MapField(c => c.ContactBusinessPhone, l => l.Telephone1); - builder.MapField(c => c.ContactCompanyName, l => l.CompanyName); - builder.MapField(c => c.ContactNotes, l => l.Description); - - return builder; - } - - public static ContactMappingBuilder AddDefaultMappingForContact(this ContactMappingBuilder builder) - { - builder.MapField(c => c.ContactFirstName, l => l.FirstName); - builder.MapField(c => c.ContactMiddleName, l => l.MiddleName); - builder.MapField(c => c.ContactLastName, l => l.LastName); - builder.MapField(c => c.ContactEmail, l => l.EMailAddress1); - builder.MapField(c => c.ContactAddress1, l => l.Address1_Line1); - builder.MapField(c => c.ContactCity, l => l.Address1_City); - builder.MapField(c => c.ContactZIP, l => l.Address1_PostalCode); - builder.MapField( - c => c.ContactCountryID > 0 - ? CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty - : string.Empty, l => l.Address1_Country); - builder.MapField(c => c.ContactJobTitle, l => l.JobTitle); - builder.MapField(c => c.ContactMobilePhone, l => l.MobilePhone); - builder.MapField(c => c.ContactBusinessPhone, l => l.Telephone1); - builder.MapField(c => c.ContactNotes, l => l.Description); - - return builder; - } -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingBuilder.cs new file mode 100644 index 0000000..6a2a00e --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingBuilder.cs @@ -0,0 +1,119 @@ +using CMS.ContactManagement; +using CMS.Globalization; +using CMS.OnlineForms; +using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Mapping; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; +using Kentico.Xperience.CRM.Dynamics.Helpers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Xrm.Sdk; +using System.Linq.Expressions; + +namespace Kentico.Xperience.CRM.Dynamics.Configuration; + +public class DynamicsContactMappingBuilder : ContactMappingBuilder +{ + private readonly IServiceCollection serviceCollection; + + public DynamicsContactMappingBuilder(IServiceCollection serviceCollection) : base(serviceCollection) + { + this.serviceCollection = serviceCollection; + } + + public DynamicsContactMappingBuilder MapField(string contactFieldName, + Expression> expression) + where TCRMEntity : Entity + { + string crmFieldName = EntityHelper.GetLogicalNameFromExpression(expression); + if (crmFieldName == string.Empty) + { + throw new InvalidOperationException("Attribute name cannot be empty"); + } + + MapField(contactFieldName, crmFieldName); + return this; + } + + public DynamicsContactMappingBuilder MapField( + Func contactInfoMappingFunc, Expression> expression) + where TCRMEntity : Entity + { + string crmFieldName = EntityHelper.GetLogicalNameFromExpression(expression); + + if (crmFieldName == string.Empty) + { + throw new InvalidOperationException("Attribute name cannot be empty"); + } + + return MapField(contactInfoMappingFunc, crmFieldName); + } + + public DynamicsContactMappingBuilder AddDefaultMappingForLead() + { + MapField(c => c.ContactFirstName, l => l.FirstName); + MapField(c => c.ContactMiddleName, l => l.MiddleName); + MapField(c => c.ContactLastName, l => l.LastName); + MapField(c => c.ContactEmail, l => l.EMailAddress1); + MapField(c => c.ContactAddress1, l => l.Address1_Line1); + MapField(c => c.ContactCity, l => l.Address1_City); + MapField(c => c.ContactZIP, l => l.Address1_PostalCode); + MapField( + c => c.ContactCountryID > 0 ? + CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty : + string.Empty, l => l.Address1_Country); + MapField(c => c.ContactJobTitle, l => l.JobTitle); + MapField(c => c.ContactMobilePhone, l => l.MobilePhone); + MapField(c => c.ContactBusinessPhone, l => l.Telephone1); + MapField(c => c.ContactCompanyName, l => l.CompanyName); + MapField(c => c.ContactNotes, l => l.Description); + + return this; + } + + public DynamicsContactMappingBuilder AddDefaultMappingForContact() + { + MapField(c => c.ContactFirstName, l => l.FirstName); + MapField(c => c.ContactMiddleName, l => l.MiddleName); + MapField(c => c.ContactLastName, l => l.LastName); + MapField(c => c.ContactEmail, l => l.EMailAddress1); + MapField(c => c.ContactAddress1, l => l.Address1_Line1); + MapField(c => c.ContactCity, l => l.Address1_City); + MapField(c => c.ContactZIP, l => l.Address1_PostalCode); + MapField( + c => c.ContactCountryID > 0 ? + CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty : + string.Empty, l => l.Address1_Country); + MapField(c => c.ContactJobTitle, l => l.JobTitle); + MapField(c => c.ContactMobilePhone, l => l.MobilePhone); + MapField(c => c.ContactBusinessPhone, l => l.Telephone1); + MapField(c => c.ContactNotes, l => l.Description); + + return this; + } + + public DynamicsContactMappingBuilder AddContactToLeadConverter() + where TConverter : class, ICRMTypeConverter + { + converters.Add(typeof(TConverter)); + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, TConverter>()); + return this; + } + + public DynamicsContactMappingBuilder AddContactToContactConverter() + where TConverter : class, ICRMTypeConverter + { + converters.Add(typeof(TConverter)); + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, TConverter>()); + return this; + } + + public DynamicsContactMappingConfiguration Build() => + new() + { + FieldsMapping = fieldMappings, + Converters = converters + }; +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index aeb1cbf..2ebfaa8 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Configuration; using Kentico.Xperience.CRM.Common.Enums; -using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Services; using Microsoft.Extensions.Configuration; @@ -54,27 +53,35 @@ public static IServiceCollection AddDynamicsContactsIntegration(this IServiceCol => serviceCollection.AddDynamicsContactsIntegration(crmType, b => { }, configuration); public static IServiceCollection AddDynamicsContactsIntegration(this IServiceCollection serviceCollection, - ContactCRMType crmType, Action mappingConfig, IConfiguration configuration, + ContactCRMType crmType, Action mappingConfig, IConfiguration? configuration = null, bool useDefaultMapping = true) { - serviceCollection.AddKenticoCrmCommonContactIntegration(mappingConfig); + serviceCollection.AddKenticoCrmCommonContactIntegration(); + + var mappingBuilder = new DynamicsContactMappingBuilder(serviceCollection); + if (useDefaultMapping) + { + mappingBuilder = crmType == ContactCRMType.Lead ? + mappingBuilder.AddDefaultMappingForLead() : + mappingBuilder.AddDefaultMappingForContact(); + mappingConfig(mappingBuilder); + } + serviceCollection.TryAddSingleton( - sp => - { - var mappingBuilder = new ContactMappingBuilder(); - if (useDefaultMapping) - { - mappingBuilder = crmType == ContactCRMType.Lead ? - mappingBuilder.AddDefaultMappingForLead() : - mappingBuilder.AddDefaultMappingForContact(); - mappingConfig(mappingBuilder); - } - - return mappingBuilder.Build(); - }); - - serviceCollection.AddOptions().Bind(configuration) - .PostConfigure(s => s.ContactType = crmType); + _ => mappingBuilder.Build()); + + if (configuration is null) + { + serviceCollection.AddOptions() + .Configure(ConfigureWithCMSSettings) + .PostConfigure(s => s.ContactType = crmType); + } + else + { + serviceCollection.AddOptions().Bind(configuration) + .PostConfigure(s => s.ContactType = crmType); + } + serviceCollection.TryAddSingleton(GetCrmServiceClient); serviceCollection.AddScoped(); @@ -108,6 +115,9 @@ private static void ConfigureWithCMSSettings(DynamicsIntegrationSettings setting { settings.FormLeadsEnabled = ValidationHelper.GetBoolean(settingsService[SettingKeys.DynamicsFormLeadsEnabled], false); + + settings.ContactsEnabled = + ValidationHelper.GetBoolean(settingsService[SettingKeys.SalesForceContactsEnabled], false); settings.IgnoreExistingRecords = ValidationHelper.GetBoolean(settingsService[SettingKeys.DynamicsIgnoreExistingRecords], false); diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactMappingBuilderExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactMappingBuilderExtensions.cs deleted file mode 100644 index cfa7dd8..0000000 --- a/src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactMappingBuilderExtensions.cs +++ /dev/null @@ -1,80 +0,0 @@ -using CMS.ContactManagement; -using CMS.Globalization; -using Kentico.Xperience.CRM.Common.Configuration; -using Kentico.Xperience.CRM.Common.Mapping.Implementations; -using SalesForce.OpenApi; -using System.Linq.Expressions; - -namespace Kentico.Xperience.CRM.SalesForce.Configuration; - -public static class ContactMappingBuilderExtensions -{ - public static ContactMappingBuilder MapLeadField(this ContactMappingBuilder builder, string contactFieldName, - Expression> expression) - { - return builder.AddMapping(new ContactFieldToCRMMapping(new ContactFieldNameMapping(contactFieldName), - new CRMFieldMappingFunction(expression))); - } - - public static ContactMappingBuilder MapLeadField(this ContactMappingBuilder builder, - Func contactInfoMappingFunc, - Expression> expression) - { - return builder.AddMapping(new ContactFieldToCRMMapping(new ContactFieldMappingFunction(contactInfoMappingFunc), - new CRMFieldMappingFunction(expression))); - } - - public static ContactMappingBuilder MapContactField(this ContactMappingBuilder builder, string contactFieldName, - Expression> expression) - { - return builder.AddMapping(new ContactFieldToCRMMapping(new ContactFieldNameMapping(contactFieldName), - new CRMFieldMappingFunction(expression))); - } - - public static ContactMappingBuilder MapContactField(this ContactMappingBuilder builder, - Func contactInfoMappingFunc, - Expression> expression) - { - return builder.AddMapping(new ContactFieldToCRMMapping(new ContactFieldMappingFunction(contactInfoMappingFunc), - new CRMFieldMappingFunction(expression))); - } - - public static ContactMappingBuilder AddDefaultMappingForLead(this ContactMappingBuilder builder) - { - builder.MapLeadField(c => c.ContactFirstName, l => l.FirstName); - builder.MapLeadField(c => c.ContactLastName, l => l.LastName); - builder.MapLeadField(c => c.ContactEmail, l => l.Email); - builder.MapLeadField(c => c.ContactAddress1, l => l.Street); - builder.MapLeadField(c => c.ContactCity, l => l.City); - builder.MapLeadField(c => c.ContactZIP, l => l.PostalCode); - builder.MapLeadField( - c => c.ContactCountryID > 0 - ? CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty - : string.Empty, l => l.Country); - builder.MapLeadField(c => c.ContactMobilePhone, l => l.MobilePhone); - builder.MapLeadField(c => c.ContactBusinessPhone, l => l.Phone); - builder.MapLeadField(c => c.ContactCompanyName, l => l.Company); - builder.MapLeadField(c => c.ContactNotes, l => l.Description); - - return builder; - } - - public static ContactMappingBuilder AddDefaultMappingForContact(this ContactMappingBuilder builder) - { - builder.MapContactField(c => c.ContactFirstName, l => l.FirstName); - builder.MapContactField(c => c.ContactLastName, l => l.LastName); - builder.MapContactField(c => c.ContactEmail, l => l.Email); - builder.MapContactField(c => c.ContactAddress1, l => l.MailingStreet); - builder.MapContactField(c => c.ContactCity, l => l.MailingCity); - builder.MapContactField(c => c.ContactZIP, l => l.MailingPostalCode); - builder.MapContactField( - c => c.ContactCountryID > 0 - ? CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty - : string.Empty, l => l.MailingCountry); - builder.MapContactField(c => c.ContactMobilePhone, l => l.MobilePhone); - builder.MapContactField(c => c.ContactBusinessPhone, l => l.Phone); - builder.MapContactField(c => c.ContactNotes, l => l.Description); - - return builder; - } -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceContactMappingBuilder.cs new file mode 100644 index 0000000..bc257a6 --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceContactMappingBuilder.cs @@ -0,0 +1,120 @@ +using CMS.ContactManagement; +using CMS.Globalization; +using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Mapping; +using Kentico.Xperience.CRM.Common.Mapping.Implementations; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using SalesForce.OpenApi; +using System.Linq.Expressions; + +namespace Kentico.Xperience.CRM.SalesForce.Configuration; + +public class SalesForceContactMappingBuilder : ContactMappingBuilder +{ + private readonly IServiceCollection serviceCollection; + + public SalesForceContactMappingBuilder(IServiceCollection serviceCollection) : base(serviceCollection) + { + this.serviceCollection = serviceCollection; + } + + public SalesForceContactMappingBuilder MapLeadField(string contactFieldName, + Expression> expression) + { + fieldMappings.Add(new ContactFieldToCRMMapping(new ContactFieldNameMapping(contactFieldName), + new CRMFieldMappingFunction(expression))); + return this; + } + + public SalesForceContactMappingBuilder MapLeadField( + Func contactInfoMappingFunc, + Expression> expression) + { + fieldMappings.Add(new ContactFieldToCRMMapping(new ContactFieldMappingFunction(contactInfoMappingFunc), + new CRMFieldMappingFunction(expression))); + return this; + } + + public SalesForceContactMappingBuilder MapContactField(string contactFieldName, + Expression> expression) + { + fieldMappings.Add(new ContactFieldToCRMMapping(new ContactFieldNameMapping(contactFieldName), + new CRMFieldMappingFunction(expression))); + return this; + } + + public SalesForceContactMappingBuilder MapContactField( + Func contactInfoMappingFunc, + Expression> expression) + { + fieldMappings.Add(new ContactFieldToCRMMapping(new ContactFieldMappingFunction(contactInfoMappingFunc), + new CRMFieldMappingFunction(expression))); + return this; + } + + public SalesForceContactMappingBuilder AddDefaultMappingForLead() + { + MapLeadField(c => c.ContactFirstName, l => l.FirstName); + MapLeadField(c => c.ContactLastName, l => l.LastName); + MapLeadField(c => c.ContactEmail, l => l.Email); + MapLeadField(c => c.ContactAddress1, l => l.Street); + MapLeadField(c => c.ContactCity, l => l.City); + MapLeadField(c => c.ContactZIP, l => l.PostalCode); + MapLeadField( + c => c.ContactCountryID > 0 + ? CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty + : string.Empty, l => l.Country); + MapLeadField(c => c.ContactMobilePhone, l => l.MobilePhone); + MapLeadField(c => c.ContactBusinessPhone, l => l.Phone); + MapLeadField(c => c.ContactCompanyName, l => l.Company); + MapLeadField(c => c.ContactNotes, l => l.Description); + + return this; + } + + public SalesForceContactMappingBuilder AddDefaultMappingForContact() + { + MapContactField(c => c.ContactFirstName, l => l.FirstName); + MapContactField(c => c.ContactLastName, l => l.LastName); + MapContactField(c => c.ContactEmail, l => l.Email); + MapContactField(c => c.ContactAddress1, l => l.MailingStreet); + MapContactField(c => c.ContactCity, l => l.MailingCity); + MapContactField(c => c.ContactZIP, l => l.MailingPostalCode); + MapContactField( + c => c.ContactCountryID > 0 + ? CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty + : string.Empty, l => l.MailingCountry); + MapContactField(c => c.ContactMobilePhone, l => l.MobilePhone); + MapContactField(c => c.ContactBusinessPhone, l => l.Phone); + MapContactField(c => c.ContactNotes, l => l.Description); + + return this; + } + + public SalesForceContactMappingBuilder AddContactToLeadConverter() + where TConverter : class, ICRMTypeConverter + { + converters.Add(typeof(TConverter)); + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, TConverter>()); + return this; + } + + public SalesForceContactMappingBuilder AddContactToContactConverter() + where TConverter : class, ICRMTypeConverter + { + converters.Add(typeof(TConverter)); + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, TConverter>()); + return this; + } + + public SalesForceContactMappingConfiguration Build() => + new() + { + FieldsMapping = fieldMappings, + Converters = converters + }; +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Kentico.Xperience.CRM.SalesForce.csproj b/src/Kentico.Xperience.CRM.SalesForce/Kentico.Xperience.CRM.SalesForce.csproj index 68e37ca..efe36b1 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Kentico.Xperience.CRM.SalesForce.csproj +++ b/src/Kentico.Xperience.CRM.SalesForce/Kentico.Xperience.CRM.SalesForce.csproj @@ -33,5 +33,6 @@ + diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index 9516bff..251167a 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -58,25 +58,22 @@ public static IServiceCollection AddSalesForceContactsIntegration(this IServiceC public static IServiceCollection AddSalesForceContactsIntegration(this IServiceCollection serviceCollection, ContactCRMType crmType, - Action mappingConfig, + Action mappingConfig, IConfiguration configuration, bool useDefaultMapping = true) { - serviceCollection.AddKenticoCrmCommonContactIntegration(mappingConfig); - serviceCollection.TryAddSingleton( - sp => - { - var mappingBuilder = new ContactMappingBuilder(); - if (useDefaultMapping) - { - mappingBuilder = crmType == ContactCRMType.Lead ? - mappingBuilder.AddDefaultMappingForLead() : - mappingBuilder.AddDefaultMappingForContact(); - mappingConfig(mappingBuilder); - } - - return mappingBuilder.Build(); - }); + serviceCollection.AddKenticoCrmCommonContactIntegration(); + + var mappingBuilder = new SalesForceContactMappingBuilder(serviceCollection); + if (useDefaultMapping) + { + mappingBuilder = crmType == ContactCRMType.Lead ? + mappingBuilder.AddDefaultMappingForLead() : + mappingBuilder.AddDefaultMappingForContact(); + mappingConfig(mappingBuilder); + } + + serviceCollection.TryAddSingleton(_ => mappingBuilder.Build()); serviceCollection.AddOptions().Bind(configuration) .PostConfigure(s => s.ContactType = crmType); @@ -132,6 +129,9 @@ private static void ConfigureWithCMSSettings(SalesForceIntegrationSettings setti settings.FormLeadsEnabled = ValidationHelper.GetBoolean(settingsService[SettingKeys.SalesForceFormLeadsEnabled], false); + settings.ContactsEnabled = + ValidationHelper.GetBoolean(settingsService[SettingKeys.SalesForceContactsEnabled], false); + settings.IgnoreExistingRecords = ValidationHelper.GetBoolean(settingsService[SettingKeys.SalesForceIgnoreExistingRecords], false); diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs index 44eec93..7cb919d 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs @@ -39,10 +39,6 @@ public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) lead.LeadSource ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; lead.Company ??= "undefined"; //required field - set to 'undefined' to prevent errors - // if (bizFormMappingConfig.ExternalIdFieldName is { Length: > 0 } externalIdFieldName) - // { - // lead.AdditionalProperties[externalIdFieldName] = FormatExternalId(bizFormItem); - // } await apiService.CreateLeadAsync(lead); } diff --git a/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json b/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json index 06a4cc7..575475e 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json +++ b/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json @@ -174,8 +174,8 @@ }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "yuvf07qFWFqtK3P/MRkEKLhn5r2UbSpVueRziSqj0yJQIKFwG1pq9mOayK3zE5qZCTs0CbrwL9M6R8VwqyGy2w==" + "resolved": "5.0.0", + "contentHash": "W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==" }, "Microsoft.CSharp": { "type": "Transitive", @@ -356,6 +356,11 @@ "resolved": "6.0.4", "contentHash": "K14wYgwOfKVELrUh5eBqlC8Wvo9vvhS3ZhIvcswV2uS/ubkTRPSQsN557EZiYUSSoZNxizG+alN4wjtdyLdcyw==" }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "5.0.10", + "contentHash": "pp9tbGqIhdEXL6Q1yJl+zevAJSq4BsxqhS1GXzBvEsEz9DDNu9GLNzgUy2xyFc4YjB4m4Ff2YEWTnvQvVYdkvQ==" + }, "Microsoft.Extensions.Options": { "type": "Transitive", "resolved": "6.0.0", @@ -464,11 +469,24 @@ "resolved": "1.1.0", "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" }, + "Microsoft.Rest.ClientRuntime": { + "type": "Transitive", + "resolved": "2.3.24", + "contentHash": "hZH7XgM3eV2jFrnq7Yf0nBD4WVXQzDrer2gEY7HMNiwio2hwDsTHO6LWuueNQAfRpNp4W7mKxcXpwXUiuVIlYw==", + "dependencies": { + "Newtonsoft.Json": "10.0.3" + } + }, "Microsoft.SqlServer.Server": { "type": "Transitive", "resolved": "1.0.0", "contentHash": "N4KeF3cpcm1PUHym1RmakkzfkEv3GRMyofVv40uXsQhCQeglr2OHNcUk2WOG51AKpGO8ynGpo9M/kFXSzghwug==" }, + "Microsoft.VisualBasic": { + "type": "Transitive", + "resolved": "10.3.0", + "contentHash": "AvMDjmJHjz9bdlvxiSdEHHcWP+sZtp7zwule5ab6DaUbgoBnwCsd7nymj69vSz18ypXuEv3SI7ZUNwbIKrvtMA==" + }, "Microsoft.Win32.SystemEvents": { "type": "Transitive", "resolved": "6.0.0", @@ -500,6 +518,86 @@ "resolved": "13.18.2", "contentHash": "SRQ3mONkfbJWhZxA1yOFMJMXavUvnXwcoYU23qoVqSEY2G+y8jZuq9ErWm76JT0Kn5/Ml5UhG1FWmLhkqd4/+A==" }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "7VSGO0URRKoMEAq0Sc9cRz8mb6zbyx/BZDEWhgPdzzpmFhkam3fJ1DAGWFXBI4nGlma+uPKpfuMQP5LXRnOH5g==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "0oAaTAm6e2oVH+/Zttt0cuhGaePQYKII1dY8iaqP7CvOpVKgLybKRFvQjXR2LtxXOXTVPNv14j0ot8uV+HrUmw==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "G24ibsCNi5Kbz0oXWynBoRgtGvsw5ZSVEWjv13/KiCAM8C6wz9zzcCniMeQFIkJ2tasjo2kXlvlBZhplL51kGg==" + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.1", + "contentHash": "UPrVPlqPRSVZaB4ADmbsQ77KXn9ORiWXyA1RP2W2+byCh3bhgT1bQz0jbeOoog9/2oTQ5wWZSDSMeb74MjezcA==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.1" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "QR1OwtwehHxSeQvZKXe+iSd+d3XZNkEcuWMFYa2i0aG1l+lR739HPicKMlTbJst3spmeekDVBUS7SeS26s4U/g==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "I+GNKGg2xCHueRd1m9PzeEW7WLbNNLznmTuEi8/vZX71HudUbx1UTwlGkiwMri7JLl8hGaIAWnA/GONhu+LOyQ==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "1Z3TAq1ytS1IBRtPXJvEUZdVsfWfeNEhBkbiOCGEl9wwAfsjP2lz3ZFDx5tq8p60/EqbS0HItG5piHuB71RjoA==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.1", + "contentHash": "t15yGf5r6vMV1rB5O6TgfXKChtCaN3niwFw44M2ImX3eZ8yzueplqMqXPCbWzoBDHJVz9fE+9LFUGCsUmS2Jgg==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "6mU/cVmmHtQiDXhnzUImxIcDL48GbTk+TsptXyJA+MIOG9LRjPoAQC/qBFB7X+UNyK86bmvGwC8t+M66wsYC8w==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "vjwG0GGcTW/PPg6KVud8F9GLWYuAV1rrw1BKAqY0oh4jcUqg15oYF1+qkGR2x2ZHM4DQnWKQ7cJgYbfncz/lYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "7KMFpTkHC/zoExs+PwP8jDCWcrK9H6L7soowT80CUx3e+nxP/AFnq0AQAW5W76z2WYbLAYCRyPfwYFG6zkvQRw==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "xrlmRCnKZJLHxyyLIqkZjNXqgxnKdZxfItrPkjI+6pkRo5lHX8YvSZlWrSI5AVwLMi4HbNWP7064hcAWeZKp5w==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "leXiwfiIkW7Gmn7cgnNcdtNAU70SjmKW3jxGj1iKHOvdn0zRWsgv/l2OJUO5zdGdiv2VRFnAsxxhDgMzofPdWg==" + }, "System.Buffers": { "type": "Transitive", "resolved": "4.5.1", @@ -510,6 +608,33 @@ "resolved": "7.0.0", "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, "System.Collections.Immutable": { "type": "Transitive", "resolved": "7.0.0", @@ -527,6 +652,16 @@ "System.Security.Permissions": "6.0.0" } }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", "resolved": "6.0.0", @@ -535,6 +670,26 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, "System.Drawing.Common": { "type": "Transitive", "resolved": "6.0.0", @@ -548,6 +703,16 @@ "resolved": "7.0.0", "contentHash": "+nfpV0afLmvJW8+pLlHxRjz3oZJw4fkyU9MMEaMhCsHi/SN9bGF9q79ROubDiwTiCHezmK0uCWkPP7tGFP/4yg==" }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", "resolved": "6.31.0", @@ -557,6 +722,77 @@ "Microsoft.IdentityModel.Tokens": "6.31.0" } }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, "System.Memory": { "type": "Transitive", "resolved": "4.5.4", @@ -576,6 +812,151 @@ "resolved": "4.5.0", "contentHash": "QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==" }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Private.DataContractSerialization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yDaJ2x3mMmjdZEDB4IbezSnCsnjQ4BxinKhRAaP6kEgL6Bb6jANWphs5SzyD8imqeC/3FxgsuXT6ykkiH1uUmA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Serialization.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0", + "System.Xml.XmlDocument": "4.3.0", + "System.Xml.XmlSerializer": "4.3.0" + } + }, + "System.Private.ServiceModel": { + "type": "Transitive", + "resolved": "4.10.2", + "contentHash": "bi2/w2EDXqxno8zfbt6vHcrpGw0Pav8tEMzmJraHwJvWYJd45wcqr7gNa2IUs91j4z+BNGMooStaWS6pm2Lq0A==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "5.0.0", + "Microsoft.Extensions.ObjectPool": "5.0.10", + "System.Numerics.Vectors": "4.5.0", + "System.Reflection.DispatchProxy": "4.7.1", + "System.Security.Cryptography.Xml": "6.0.1", + "System.Security.Principal.Windows": "5.0.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.DispatchProxy": { + "type": "Transitive", + "resolved": "4.7.1", + "contentHash": "C1sMLwIG6ILQ2bmOT4gh62V6oJlyF4BlHcVMrOoor49p0Ji2tA8QAoqyMcIhAdH6OHKJ8m7BU+r4LK2CUEOKqw==" + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", + "dependencies": { + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VybpaOQQhqE6siHppMktjfGBw1GCwvCqiufqmP8F1nj7fTUNtW35LOEt3UZTEsECfo+ELAl/9o9nJx3U91i7vA==" + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, "System.Runtime": { "type": "Transitive", "resolved": "4.3.0", @@ -598,11 +979,98 @@ "resolved": "6.0.0", "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Runtime.Serialization.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", + "dependencies": { + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Serialization.Xml": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nUQx/5OVgrqEba3+j7OdiofvVq9koWZAC7Z3xGI8IIViZqApWnZ5+lLcwYgTlbkobrl/Rat+Jb8GeD4WQESD2A==", + "dependencies": { + "System.IO": "4.3.0", + "System.Private.DataContractSerialization": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Serialization.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, "System.Security.AccessControl": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "AUADIc0LIEQe7MzC+I0cl0rAT8RrTAKFHl53yHjEUzNVIaUlhFY11vc2ebiVJzVBuOzun6F7FBA+8KAbGTTedQ==" }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.1", + "contentHash": "DVUblnRfnarrI5olEC2B/OCsJQd0anjVaObQMndHSc43efbc88/RMOlDyg/EyY0ix5ecyZMXS8zMksb5ukebZA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.1", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" + } + }, "System.Security.Cryptography.Cng": { "type": "Transitive", "resolved": "5.0.0", @@ -611,6 +1079,25 @@ "System.Formats.Asn1": "5.0.0" } }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", "resolved": "7.0.2", @@ -619,11 +1106,34 @@ "System.Formats.Asn1": "7.0.0" } }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, "System.Security.Cryptography.ProtectedData": { "type": "Transitive", "resolved": "6.0.0", "contentHash": "rp1gMNEZpvx9vP0JW0oHLxlf8oSiQgtno77Y4PLUBjSiDYoD77Y8uXHr1Ea5XG4/pIKhqAdxZ8v8OTUtqo9PeQ==" }, + "System.Security.Cryptography.Xml": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "5e5bI28T0x73AwTsbuFP4qSRzthmU2C0Gqgg3AZ3KTxmSyA+Uhk31puA3srdaeWaacVnHhLdJywCzqOiEpbO/w==", + "dependencies": { + "System.Security.AccessControl": "6.0.0", + "System.Security.Cryptography.Pkcs": "6.0.1" + } + }, "System.Security.Permissions": { "type": "Transitive", "resolved": "6.0.0", @@ -638,6 +1148,23 @@ "resolved": "5.0.0", "contentHash": "t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==" }, + "System.ServiceModel.Http": { + "type": "Transitive", + "resolved": "4.10.2", + "contentHash": "1AhiJwPc+90GjBd/sDkT93RVuRj688ZQInXzWfd56AEjDzWieUcAUh9ppXhRuEVpT+x48D5GBYJc1VxDP4IT+Q==", + "dependencies": { + "System.Private.ServiceModel": "4.10.2", + "System.ServiceModel.Primitives": "4.10.2" + } + }, + "System.ServiceModel.Primitives": { + "type": "Transitive", + "resolved": "4.10.2", + "contentHash": "8QOguIqHtWYywBt7SubPhdICE2LClHzqOMDy0LQIui4T3QJOae7g6UR+alCW61nEufYNtO8Uss41EbXqD8hdww==", + "dependencies": { + "System.Private.ServiceModel": "4.10.2" + } + }, "System.Text.Encoding": { "type": "Transitive", "resolved": "4.3.0", @@ -656,6 +1183,17 @@ "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, "System.Text.Encodings.Web": { "type": "Transitive", "resolved": "6.0.0", @@ -666,8 +1204,39 @@ }, "System.Text.Json": { "type": "Transitive", - "resolved": "4.7.2", - "contentHash": "TcMd95wcrubm9nHvJEQs70rC0H/8omiSGGpU4FQ/ZA1URIqD4pjmFJh2Mfv1yH1eHgJDWTi2hMDXwTET+zOOyg==" + "resolved": "6.0.7", + "contentHash": "/Tf/9XjprpHolbcDOrxsKVYy/mUG/FS7aGd9YUgBVEiHeQH4kAE0T1sMbde7q6B5xcrNUsJ5iW7D1RvHudQNqA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encodings.Web": "6.0.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } }, "System.Threading.Tasks.Extensions": { "type": "Transitive", @@ -682,6 +1251,88 @@ "System.Drawing.Common": "6.0.0" } }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XmlDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lJ8AxvkX7GQxpC6GFCeBj8ThYVyQczx2+f/cWHJU8tjS7YfI6Cv6bon70jVEgs2CiFbmmM8b9j1oZVx0dSI2Ww==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XmlSerializer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "MYoTCP7EZ98RrANESW05J5ZwskKDoN0AuZ06ZflnowE50LTpbR5yRg3tHckTVm5j/m47stuGgCrCHWePyHS70Q==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XmlDocument": "4.3.0" + } + }, "kentico.xperience.crm.common": { "type": "Project", "dependencies": { @@ -689,6 +1340,14 @@ "Kentico.Xperience.Core": "[27.0.1, )" } }, + "Kentico.Xperience.Dynamics.CRM.Integration": { + "type": "Project", + "dependencies": { + "Kentico.Xperience.CRM.Common": "[1.0.0-prerelease-1, )", + "Kentico.Xperience.Core": "[27.0.1, )", + "Microsoft.PowerPlatform.Dataverse.Client": "[1.1.14, )" + } + }, "Kentico.Xperience.Admin": { "type": "CentralTransitive", "requested": "[27.0.1, )", @@ -713,6 +1372,43 @@ "Microsoft.Extensions.FileProviders.Embedded": "6.0.22", "Microsoft.Extensions.Localization": "6.0.22" } + }, + "Microsoft.PowerPlatform.Dataverse.Client": { + "type": "CentralTransitive", + "requested": "[1.1.14, )", + "resolved": "1.1.14", + "contentHash": "AoWyada9Y3lI88pmbCRWZt0rr5ET7uz3ntEQFfd2UxiBM9rvaijjHn/gOQpm0ERH5Ip53VxnlshtrtTFMs7RoA==", + "dependencies": { + "Microsoft.Extensions.Caching.Memory": "3.1.8", + "Microsoft.Extensions.DependencyInjection": "3.1.8", + "Microsoft.Extensions.Http": "3.1.8", + "Microsoft.Extensions.Logging": "3.1.8", + "Microsoft.Identity.Client": "4.35.1", + "Microsoft.Identity.Client.Extensions.Msal": "2.18.9", + "Microsoft.Rest.ClientRuntime": "2.3.24", + "Microsoft.VisualBasic": "10.3.0", + "Newtonsoft.Json": "13.0.1", + "System.Collections": "4.3.0", + "System.Configuration.ConfigurationManager": "4.7.0", + "System.Drawing.Common": "5.0.3", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Private.DataContractSerialization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.TypeExtensions": "4.7.0", + "System.Runtime.Caching": "4.7.0", + "System.Runtime.Serialization.Primitives": "4.3.0", + "System.Runtime.Serialization.Xml": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.1", + "System.Security.Cryptography.Pkcs": "6.0.3", + "System.Security.Cryptography.ProtectedData": "4.7.0", + "System.Security.Cryptography.Xml": "6.0.1", + "System.Security.Permissions": "5.0.0", + "System.ServiceModel.Http": "4.10.2", + "System.Text.Json": "6.0.7" + } } } } From 430cb738b2084a855c57bc65f21c24c754b47163 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Wed, 24 Jan 2024 17:31:44 +0100 Subject: [PATCH 07/26] contact sync wip --- examples/DancingGoat/Program.cs | 4 +- .../ServiceCollectionExtensions.cs | 1 + .../Services/ICRMSyncItemService.cs | 7 +- .../Implementations/CRMSyncItemService.cs | 38 +++- .../DynamicsServiceCollectionExtensions.cs | 6 +- .../DynamicsContactsIntegrationService.cs | 209 +++++++++++++++++- .../DynamicsLeadsIntegrationService.cs | 10 +- .../SalesForceServiceCollectionsExtensions.cs | 6 +- .../Services/ISalesForceApiService.cs | 38 ++++ .../Services/SalesForceApiService.cs | 25 +++ .../SalesForceContactsIntegrationService.cs | 162 +++++++++++++- 11 files changed, 474 insertions(+), 32 deletions(-) diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index e41791d..c37c039 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -94,10 +94,10 @@ // builder.Services.AddDynamicsContactsIntegration(ContactCRMType.Lead, // builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); -builder.Services.AddDynamicsContactsIntegration(ContactCRMType.Contact, +builder.Services.AddKenticoCRMDynamicsContactsIntegration(ContactCRMType.Contact, builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); -builder.Services.AddSalesForceContactsIntegration(ContactCRMType.Lead, +builder.Services.AddKenticoCRMSalesForceContactsIntegration(ContactCRMType.Lead, builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); //CRM integration registration end diff --git a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs index 977bc72..5e40b8e 100644 --- a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs @@ -23,6 +23,7 @@ public static IServiceCollection AddKenticoCrmCommonFormLeadsIntegration(this IS services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); return services; } diff --git a/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs index 9769d69..2cab81f 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs @@ -1,4 +1,5 @@ -using CMS.OnlineForms; +using CMS.ContactManagement; +using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Classes; namespace Kentico.Xperience.CRM.Common.Services; @@ -8,4 +9,8 @@ public interface ICRMSyncItemService Task LogFormLeadCreateItem(BizFormItem bizFormItem, string crmId, string crmName); Task LogFormLeadUpdateItem(BizFormItem bizFormItem, string crmId, string crmName); Task GetFormLeadSyncItem(BizFormItem bizFormItem, string crmName); + + Task LogContactCreateItem(ContactInfo contactInfo, string crmId, string crmName); + Task LogContactUpdateItem(ContactInfo contactInfo, string crmId, string crmName); + Task GetContactSyncItem(ContactInfo contactInfo, string crmName); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs index 6f018f7..d9aee08 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs @@ -1,4 +1,5 @@ -using CMS.OnlineForms; +using CMS.ContactManagement; +using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Classes; namespace Kentico.Xperience.CRM.Common.Services.Implementations; @@ -49,4 +50,39 @@ private async Task LogFormLeadSyncItem(BizFormItem bizFormItem, string crmId, st .GetEnumerableTypedResultAsync()) .FirstOrDefault(); + public Task LogContactCreateItem(ContactInfo contactInfo, string crmId, string crmName) + => LogContactSyncItem(contactInfo, crmId, crmName, createdByKentico: true); + + public Task LogContactUpdateItem(ContactInfo contactInfo, string crmId, string crmName) + => LogContactSyncItem(contactInfo, crmId, crmName, createdByKentico: false); + + private async Task LogContactSyncItem(ContactInfo contactInfo, string crmId, string crmName, bool createdByKentico) + { + var syncItem = await GetContactSyncItem(contactInfo, crmName); + if (syncItem is null) + { + new CRMSyncItemInfo + { + CRMSyncItemEntityID = contactInfo.ContactEmail, + CRMSyncItemEntityClass = contactInfo.ClassName, + CRMSyncItemEntityCRM = crmName, + CRMSyncItemCRMID = crmId, + CRMSyncItemCreatedByKentico = createdByKentico + }.Insert(); + } + else + { + syncItem.CRMSyncItemCRMID = crmId; + syncItem.Update(); + } + } + + public async Task GetContactSyncItem(ContactInfo contactInfo, string crmName) + => (await crmSyncItemInfoProvider.Get() + .TopN(1) + .WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityClass), contactInfo.ClassName) + .WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityID), contactInfo.ContactEmail) + .WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityCRM), crmName) + .GetEnumerableTypedResultAsync()) + .FirstOrDefault(); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index 8e2d548..c766bbd 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -47,11 +47,11 @@ public static IServiceCollection AddKenticoCRMDynamics(this IServiceCollection s return serviceCollection; } - public static IServiceCollection AddDynamicsContactsIntegration(this IServiceCollection serviceCollection, + public static IServiceCollection AddKenticoCRMDynamicsContactsIntegration(this IServiceCollection serviceCollection, ContactCRMType crmType, IConfiguration configuration) - => serviceCollection.AddDynamicsContactsIntegration(crmType, b => { }, configuration); + => serviceCollection.AddKenticoCRMDynamicsContactsIntegration(crmType, b => { }, configuration); - public static IServiceCollection AddDynamicsContactsIntegration(this IServiceCollection serviceCollection, + public static IServiceCollection AddKenticoCRMDynamicsContactsIntegration(this IServiceCollection serviceCollection, ContactCRMType crmType, Action mappingConfig, IConfiguration? configuration = null, bool useDefaultMapping = true) { diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs index 19421c7..b30bf69 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs @@ -1,10 +1,12 @@ using CMS.ContactManagement; using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.PowerPlatform.Dataverse.Client; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; @@ -18,19 +20,31 @@ public class DynamicsContactsIntegrationService : IDynamicsContactsIntegrationSe private readonly IContactsIntegrationValidationService validationService; private readonly ServiceClient serviceClient; private readonly ILogger logger; + private readonly ICRMSyncItemService syncItemService; private readonly IFailedSyncItemService failedSyncItemService; + private readonly IOptionsSnapshot settings; + private readonly IEnumerable> contactLeadConverters; + private readonly IEnumerable> contactContactConverters; public DynamicsContactsIntegrationService(DynamicsContactMappingConfiguration contactMapping, IContactsIntegrationValidationService validationService, ServiceClient serviceClient, ILogger logger, - IFailedSyncItemService failedSyncItemService) + ICRMSyncItemService syncItemService, + IFailedSyncItemService failedSyncItemService, + IOptionsSnapshot settings, + IEnumerable> contactLeadConverters, + IEnumerable> contactContactConverters) { this.contactMapping = contactMapping; this.validationService = validationService; this.serviceClient = serviceClient; this.logger = logger; + this.syncItemService = syncItemService; this.failedSyncItemService = failedSyncItemService; + this.settings = settings; + this.contactLeadConverters = contactLeadConverters; + this.contactContactConverters = contactContactConverters; } public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) @@ -43,12 +57,29 @@ public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) contactInfo.ContactID); return; } - var leadEntity = new Lead(); - MapCRMEntity(contactInfo, leadEntity, contactMapping.FieldsMapping); - leadEntity.Subject ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; + var syncItem = await syncItemService.GetContactSyncItem(contactInfo, CRMType.Dynamics); - await serviceClient.CreateAsync(leadEntity); + if (syncItem is null) + { + await UpdateLeadByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); + } + else + { + var existingLead = await GetEntityById(Guid.Parse(syncItem.CRMSyncItemCRMID), Lead.EntityLogicalName); + if (existingLead is null) + { + await UpdateLeadByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); + } + else if (!settings.Value.IgnoreExistingRecords) + { + await UpdateLeadAsync(existingLead, contactInfo, contactMapping.FieldsMapping); + } + else + { + logger.LogInformation("Contact {ContactEmail} ignored", contactInfo.ContactEmail); + } + } } catch (FaultException e) { @@ -66,7 +97,7 @@ public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); } } - + public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) { try @@ -77,12 +108,29 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) contactInfo.ContactID); return; } - var leadEntity = new Contact(); - MapCRMEntity(contactInfo, leadEntity, contactMapping.FieldsMapping); - //leadEntity.Subject ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; + var syncItem = await syncItemService.GetContactSyncItem(contactInfo, CRMType.Dynamics); - await serviceClient.CreateAsync(leadEntity); + if (syncItem is null) + { + await UpdateContactByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); + } + else + { + var existingContact = await GetEntityById(Guid.Parse(syncItem.CRMSyncItemCRMID), Contact.EntityLogicalName); + if (existingContact is null) + { + await UpdateContactByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); + } + else if (!settings.Value.IgnoreExistingRecords) + { + await UpdateContactAsync(existingContact, contactInfo, contactMapping.FieldsMapping); + } + else + { + logger.LogInformation("Contact {ContactEmail} ignored", contactInfo.ContactEmail); + } + } } catch (FaultException e) { @@ -101,9 +149,133 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) } } - protected virtual void MapCRMEntity(ContactInfo contactInfo, Entity leadEntity, + private async Task UpdateLeadByEmailOrCreate(ContactInfo contactInfo, + IEnumerable fieldMappings) + { + Lead? existingLead = null; + var tmpLead = new Lead(); + await MapCRMEntity(contactInfo, tmpLead, fieldMappings); + + if (!string.IsNullOrWhiteSpace(tmpLead.EMailAddress1)) + { + existingLead = await GetLeadByEmail(tmpLead.EMailAddress1, Lead.EntityLogicalName); + } + + if (existingLead is null) + { + await CreateLeadAsync(contactInfo, fieldMappings); + } + else if (!settings.Value.IgnoreExistingRecords) + { + await UpdateLeadAsync(existingLead, contactInfo, fieldMappings); + } + else + { + logger.LogInformation("Contact {ContactEmail} ignored", contactInfo.ContactEmail); + } + } + + private async Task UpdateContactByEmailOrCreate(ContactInfo contactInfo, + IEnumerable fieldMappings) + { + Contact? existingContact = null; + var tmpContact = new Contact(); + await MapCRMEntity(contactInfo, tmpContact, fieldMappings); + + if (!string.IsNullOrWhiteSpace(tmpContact.EMailAddress1)) + { + existingContact = await GetLeadByEmail(tmpContact.EMailAddress1, Contact.EntityLogicalName); + } + + if (existingContact is null) + { + await CreateContactAsync(contactInfo, fieldMappings); + } + else if (!settings.Value.IgnoreExistingRecords) + { + await UpdateContactAsync(existingContact, contactInfo, fieldMappings); + } + else + { + logger.LogInformation("Contact {ContactEmail} ignored", contactInfo.ContactEmail); + } + } + + private async Task CreateLeadAsync(ContactInfo contactInfo, IEnumerable fieldMappings) + { + var leadEntity = new Lead(); + await MapCRMEntity(contactInfo, leadEntity, fieldMappings); + + leadEntity.Subject ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; + + var leadId = await serviceClient.CreateAsync(leadEntity); + + await syncItemService.LogContactCreateItem(contactInfo, leadId.ToString(), CRMType.Dynamics); + //@TODO + // failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, contactInfo.ClassName, + // contactInfo.ContactEmail); + } + + private async Task UpdateLeadAsync(Lead leadEntity, ContactInfo contactInfo, + IEnumerable fieldMappings) + { + await MapCRMEntity(contactInfo, leadEntity, fieldMappings); + + leadEntity.Subject ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; + + await serviceClient.UpdateAsync(leadEntity); + + await syncItemService.LogContactUpdateItem(contactInfo, leadEntity.LeadId.ToString()!, CRMType.Dynamics); + //@TODO + // failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, contactInfo.ClassName, + // contactInfo.ContactEmail); + } + + private async Task CreateContactAsync(ContactInfo contactInfo, IEnumerable fieldMappings) + { + var contactEntity = new Contact(); + await MapCRMEntity(contactInfo, contactEntity, fieldMappings); + + var leadId = await serviceClient.CreateAsync(contactEntity); + + await syncItemService.LogContactCreateItem(contactInfo, leadId.ToString(), CRMType.Dynamics); + //@TODO + // failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, contactInfo.ClassName, + // contactInfo.ContactEmail); + } + + private async Task UpdateContactAsync(Contact contactEntity, ContactInfo contactInfo, + IEnumerable fieldMappings) + { + await MapCRMEntity(contactInfo, contactEntity, fieldMappings); + + await serviceClient.UpdateAsync(contactEntity); + + await syncItemService.LogContactUpdateItem(contactInfo, contactEntity.ContactId.ToString()!, CRMType.Dynamics); + //@TODO + // failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, contactInfo.ClassName, + // contactInfo.ContactEmail); + } + + protected async Task MapCRMEntity(ContactInfo contactInfo, Entity leadEntity, IEnumerable fieldMappings) { + if (leadEntity is Lead lead) + { + foreach (var converter in contactLeadConverters) + { + await converter.Convert(contactInfo, lead); + } + } + + if (leadEntity is Contact contact) + { + foreach (var converter in contactContactConverters) + { + await converter.Convert(contactInfo, contact); + } + } + foreach (var fieldMapping in fieldMappings) { var formFieldValue = fieldMapping.ContactFieldMapping.MapContactField(contactInfo); @@ -135,7 +307,20 @@ private async Task> GetModifiedEntitiesAsync(DateT { var query = new QueryExpression(entityName) { ColumnSet = new ColumnSet(true) }; query.Criteria.AddCondition("modifiedon", ConditionOperator.GreaterThan, lastSync.ToUniversalTime()); - + return (await serviceClient.RetrieveMultipleAsync(query)).Entities.Select(e => e.ToEntity()); } + + private async Task GetEntityById(Guid leadId, string logicalName) + where TEntity : Entity + => (await serviceClient.RetrieveAsync(logicalName, leadId, new ColumnSet(true)))?.ToEntity(); + + private async Task GetLeadByEmail(string email, string logicalName) + where TEntity : Entity + { + var query = new QueryExpression(Lead.EntityLogicalName) { ColumnSet = new ColumnSet(true), TopCount = 1 }; + query.Criteria.AddCondition("emailaddress1", ConditionOperator.Equal, email); + + return (await serviceClient.RetrieveMultipleAsync(query)).Entities.FirstOrDefault()?.ToEntity(); + } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs index 5228fe7..d826654 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs @@ -162,10 +162,7 @@ private async Task CreateLeadAsync(BizFormItem bizFormItem, var leadEntity = new Lead(); await MapLead(bizFormItem, leadEntity, fieldMappings, converters); - if (leadEntity.Subject is null) - { - leadEntity.Subject = $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; - } + leadEntity.Subject ??= $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; var leadId = await serviceClient.CreateAsync(leadEntity); @@ -179,10 +176,7 @@ private async Task UpdateLeadAsync(Lead leadEntity, BizFormItem bizFormItem, { await MapLead(bizFormItem, leadEntity, fieldMappings, converters); - if (leadEntity.Subject is null) - { - leadEntity.Subject = $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; - } + leadEntity.Subject ??= $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; await serviceClient.UpdateAsync(leadEntity); diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index d1b8da3..abebae8 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -51,11 +51,11 @@ public static IServiceCollection AddKenticoCRMSalesForce(this IServiceCollection return serviceCollection; } - public static IServiceCollection AddSalesForceContactsIntegration(this IServiceCollection serviceCollection, + public static IServiceCollection AddKenticoCRMSalesForceContactsIntegration(this IServiceCollection serviceCollection, ContactCRMType crmType, IConfiguration configuration) - => serviceCollection.AddSalesForceContactsIntegration(crmType, b => { }, configuration); + => serviceCollection.AddKenticoCRMSalesForceContactsIntegration(crmType, b => { }, configuration); - public static IServiceCollection AddSalesForceContactsIntegration(this IServiceCollection serviceCollection, + public static IServiceCollection AddKenticoCRMSalesForceContactsIntegration(this IServiceCollection serviceCollection, ContactCRMType crmType, Action mappingConfig, IConfiguration configuration, diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs index 3dff3ba..a255ea1 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs @@ -44,4 +44,42 @@ public interface ISalesForceApiService /// /// Task GetLeadByEmail(string email); + + /// + /// Creates lead entity to SalesForce Leads + /// + /// + /// + Task CreateContactAsync(ContactSObject contact); + + /// + /// Updates lead entity to SalesForce Leads + /// + /// SalesForce lead ID + /// + /// + Task UpdateContactAsync(string id, ContactSObject contact); + + /// + /// Get Lead ID for item by external ID + /// + /// Custom field for external ID + /// External ID value + /// + Task GetContactIdByExternalId(string fieldName, string externalId); + + /// + /// Get Lead by primary Id + /// + /// + /// + /// + Task GetContactById(string id, string? fields = null); + + /// + /// Get Lead by email + /// + /// + /// + Task GetContactByEmail(string email); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs index 7368fc7..05abcca 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs @@ -100,4 +100,29 @@ public async Task UpdateLeadAsync(string id, LeadSObject leadSObject) throw new ApiException("Unexpected response", (int)response.StatusCode, responseMessage, null!, null); } } + + public Task CreateContactAsync(ContactSObject contact) + { + throw new NotImplementedException(); + } + + public Task UpdateContactAsync(string id, ContactSObject contact) + { + throw new NotImplementedException(); + } + + public Task GetContactIdByExternalId(string fieldName, string externalId) + { + throw new NotImplementedException(); + } + + public Task GetContactById(string id, string? fields = null) + { + throw new NotImplementedException(); + } + + public Task GetContactByEmail(string email) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs index 7cb919d..f91cc17 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceContactsIntegrationService.cs @@ -1,8 +1,12 @@ using CMS.ContactManagement; +using CMS.OnlineForms; +using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.SalesForce.Configuration; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using SalesForce.OpenApi; using System.Text.Json; @@ -14,20 +18,32 @@ internal class SalesForceContactsIntegrationService : ISalesForceContactsIntegra private readonly IContactsIntegrationValidationService validationService; private readonly ISalesForceApiService apiService; private readonly ILogger logger; + private readonly ICRMSyncItemService syncItemService; private readonly IFailedSyncItemService failedSyncItemService; + private readonly IOptionsSnapshot settings; + private readonly IEnumerable> contactLeadConverters; + private readonly IEnumerable> contactContactConverters; public SalesForceContactsIntegrationService( SalesForceContactMappingConfiguration contactMapping, IContactsIntegrationValidationService validationService, ISalesForceApiService apiService, ILogger logger, - IFailedSyncItemService failedSyncItemService) + ICRMSyncItemService syncItemService, + IFailedSyncItemService failedSyncItemService, + IOptionsSnapshot settings, + IEnumerable> contactLeadConverters, + IEnumerable> contactContactConverters) { this.contactMapping = contactMapping; this.validationService = validationService; this.apiService = apiService; this.logger = logger; + this.syncItemService = syncItemService; this.failedSyncItemService = failedSyncItemService; + this.settings = settings; + this.contactLeadConverters = contactLeadConverters; + this.contactContactConverters = contactContactConverters; } public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) @@ -73,10 +89,131 @@ public Task> GetModifiedContactsAsync(DateTime lastS { throw new NotImplementedException(); } + + private async Task UpdateLeadByEmailOrCreate(ContactInfo contactInfo, IEnumerable fieldMappings) + { + string? existingLeadId = null; + + var tmpLead = new LeadSObject(); + await MapLead(contactInfo, tmpLead, fieldMappings); + + if (!string.IsNullOrWhiteSpace(tmpLead.Email)) + { + existingLeadId = await apiService.GetLeadByEmail(tmpLead.Email); + } + + if (existingLeadId is null) + { + await CreateLeadAsync(contactInfo, fieldMappings); + } + else if (!settings.Value.IgnoreExistingRecords) + { + await UpdateLeadAsync(existingLeadId, contactInfo, fieldMappings); + } + else + { + logger.LogInformation("Contact {ContactEmail} ignored", contactInfo.ContactEmail); + } + } + + private async Task UpdateContactByEmailOrCreate(ContactInfo contactInfo, IEnumerable fieldMappings) + { + string? existingLeadId = null; + + var tmpLead = new ContactSObject(); + await MapContact(contactInfo, tmpLead, fieldMappings); + + if (!string.IsNullOrWhiteSpace(tmpLead.Email)) + { + existingLeadId = await apiService.GetContactByEmail(tmpLead.Email); + } + + if (existingLeadId is null) + { + await CreateContactAsync(contactInfo, fieldMappings); + } + else if (!settings.Value.IgnoreExistingRecords) + { + await UpdateContactAsync(existingLeadId, contactInfo, fieldMappings); + } + else + { + logger.LogInformation("Contact {ContactEmail} ignored", contactInfo.ContactEmail); + } + } + + private async Task CreateLeadAsync(ContactInfo contactInfo, IEnumerable fieldMappings) + { + var lead = new LeadSObject(); + await MapLead(contactInfo, lead, fieldMappings); + + lead.LeadSource ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; + + lead.Company ??= "undefined"; //required field - set to 'undefined' to prevent errors + + var result = await apiService.CreateLeadAsync(lead); + + await syncItemService.LogContactCreateItem(contactInfo, result.Id!, CRMType.SalesForce); + //@TODO + // failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, contactInfo.BizFormClassName, + // contactInfo.ItemID); + } + + private async Task UpdateLeadAsync(string leadId, ContactInfo contactInfo, + IEnumerable fieldMappings) + { + var lead = new LeadSObject(); + await MapLead(contactInfo, lead, fieldMappings); + + lead.LeadSource ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; + + await apiService.UpdateLeadAsync(leadId, lead); + + await syncItemService.LogContactUpdateItem(contactInfo, leadId, CRMType.SalesForce); + //@TODO + // failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, contactInfo.BizFormClassName, + // contactInfo.ItemID); + } + + private async Task CreateContactAsync(ContactInfo contactInfo, IEnumerable fieldMappings) + { + var contact = new ContactSObject(); + await MapContact(contactInfo, contact, fieldMappings); + + contact.LeadSource ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; + + var result = await apiService.CreateContactAsync(contact); + + await syncItemService.LogContactCreateItem(contactInfo, result.Id!, CRMType.SalesForce); + //@TODO + // failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, contactInfo.BizFormClassName, + // contactInfo.ItemID); + } + + private async Task UpdateContactAsync(string leadId, ContactInfo contactInfo, + IEnumerable fieldMappings) + { + var contact = new ContactSObject(); + await MapContact(contactInfo, contact, fieldMappings); + + contact.LeadSource ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; + + await apiService.UpdateContactAsync(leadId, contact); + + await syncItemService.LogContactUpdateItem(contactInfo, leadId, CRMType.SalesForce); + //@TODO + // failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, contactInfo.BizFormClassName, + // contactInfo.ItemID); + } - protected virtual void MapLead(ContactInfo contactInfo, LeadSObject lead, + protected async Task MapLead(ContactInfo contactInfo, LeadSObject lead, IEnumerable fieldMappings) { + foreach (var converter in contactLeadConverters) + { + lead = await converter.Convert(contactInfo, lead); + } + foreach (var fieldMapping in fieldMappings) { var formFieldValue = fieldMapping.ContactFieldMapping.MapContactField(contactInfo); @@ -89,4 +226,25 @@ protected virtual void MapLead(ContactInfo contactInfo, LeadSObject lead, }; } } + + protected async Task MapContact(ContactInfo contactInfo, ContactSObject contact, + IEnumerable fieldMappings) + { + foreach (var converter in contactContactConverters) + { + contact = await converter.Convert(contactInfo, contact); + } + + foreach (var fieldMapping in fieldMappings) + { + var formFieldValue = fieldMapping.ContactFieldMapping.MapContactField(contactInfo); + _ = fieldMapping.CRMFieldMapping switch + { + CRMFieldNameMapping m => contact.AdditionalProperties[m.CrmFieldName] = formFieldValue, + CRMFieldMappingFunction m => m.MapCrmField(contact, formFieldValue), + _ => throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), + fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping") + }; + } + } } \ No newline at end of file From 26b1339c866764f2659b341fb270b7a34640f1c1 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Thu, 25 Jan 2024 01:23:25 +0100 Subject: [PATCH 08/26] salesforce contacts api --- .../ISalesforceApiService.cs | 20 +---- .../SalesForceContactsIntegrationService.cs | 83 ++++++++++++++++-- .../SalesforceApiService.cs | 84 +++++-------------- 3 files changed, 97 insertions(+), 90 deletions(-) diff --git a/src/Kentico.Xperience.CRM.Salesforce/ISalesforceApiService.cs b/src/Kentico.Xperience.CRM.Salesforce/ISalesforceApiService.cs index 3463e79..614f2c4 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/ISalesforceApiService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/ISalesforceApiService.cs @@ -21,15 +21,7 @@ public interface ISalesforceApiService /// /// Task UpdateLeadAsync(string id, LeadSObject leadSObject); - - /// - /// Get Lead ID for item by external ID - /// - /// Custom field for external ID - /// External ID value - /// - Task GetLeadIdByExternalId(string fieldName, string externalId); - + /// /// Get Lead by primary Id /// @@ -59,15 +51,7 @@ public interface ISalesforceApiService /// /// Task UpdateContactAsync(string id, ContactSObject contact); - - /// - /// Get Lead ID for item by external ID - /// - /// Custom field for external ID - /// External ID value - /// - Task GetContactIdByExternalId(string fieldName, string externalId); - + /// /// Get Lead by primary Id /// diff --git a/src/Kentico.Xperience.CRM.Salesforce/SalesForceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Salesforce/SalesForceContactsIntegrationService.cs index 91d5928..b101f22 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/SalesForceContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/SalesForceContactsIntegrationService.cs @@ -3,6 +3,7 @@ using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; using Kentico.Xperience.CRM.Salesforce.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -49,13 +50,35 @@ public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) { try { - var lead = new LeadSObject(); - MapLead(contactInfo, lead, contactMapping.FieldsMapping); + if (!await validationService.ValidateContactInfo(contactInfo)) + { + logger.LogInformation("Contact info {ItemID} refused by validation", + contactInfo.ContactID); + return; + } - lead.LeadSource ??= $"Contact {contactInfo.ContactEmail} - ID: {contactInfo.ContactID}"; - lead.Company ??= "undefined"; //required field - set to 'undefined' to prevent errors + var syncItem = await syncItemService.GetContactSyncItem(contactInfo, CRMType.Dynamics); - await apiService.CreateLeadAsync(lead); + if (syncItem is null) + { + await UpdateLeadByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); + } + else + { + var existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemEntityID, nameof(LeadSObject.Id)); + if (existingLead is null) + { + await UpdateLeadByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); + } + else if (!settings.Value.IgnoreExistingRecords) + { + await UpdateLeadAsync(existingLead.Id!, contactInfo, contactMapping.FieldsMapping); + } + else + { + logger.LogInformation("Contact {ContactEmail} ignored", contactInfo.ContactEmail); + } + } } catch (ApiException> e) { @@ -74,9 +97,55 @@ public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) } } - public Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) + public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) { - throw new NotImplementedException(); + try + { + if (!await validationService.ValidateContactInfo(contactInfo)) + { + logger.LogInformation("Contact {ContactEmail} refused by validation", + contactInfo.ContactEmail); + return; + } + + var syncItem = await syncItemService.GetContactSyncItem(contactInfo, CRMType.Dynamics); + + if (syncItem is null) + { + await UpdateContactByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); + } + else + { + var existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemEntityID, nameof(LeadSObject.Id)); + if (existingLead is null) + { + await UpdateContactByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); + } + else if (!settings.Value.IgnoreExistingRecords) + { + await UpdateContactAsync(existingLead.Id!, contactInfo, contactMapping.FieldsMapping); + } + else + { + logger.LogInformation("Contact {ContactEmail} ignored", contactInfo.ContactEmail); + } + } + } + catch (ApiException> e) + { + logger.LogError(e, "Create lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); + //failedSyncItemService.LogFailedLeadItem(contactInfo, CRMType.SalesForce); + } + catch (ApiException> e) + { + logger.LogError(e, "Create lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); + //failedSyncItemService.LogFailedLeadItem(contactInfo, CRMType.SalesForce); + } + catch (ApiException e) + { + logger.LogError(e, "Create lead failed - unexpected api error"); + //failedSyncItemService.LogFailedLeadItem(contactInfo, CRMType.SalesForce); + } } public Task> GetModifiedLeadsAsync(DateTime lastSync) diff --git a/src/Kentico.Xperience.CRM.Salesforce/SalesforceApiService.cs b/src/Kentico.Xperience.CRM.Salesforce/SalesforceApiService.cs index af6eddc..582f09f 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/SalesforceApiService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/SalesforceApiService.cs @@ -34,58 +34,37 @@ IOptionsSnapshot integrationSettings } public async Task CreateLeadAsync(LeadSObject lead) - { - return await apiClient.LeadPOSTAsync(MediaTypeNames.Application.Json, lead); - } + => await apiClient.LeadPOSTAsync(MediaTypeNames.Application.Json, lead); + public async Task UpdateLeadAsync(string id, LeadSObject leadSObject) - { - await apiClient.LeadPATCHAsync(id, MediaTypeNames.Application.Json, leadSObject); - } + => await apiClient.LeadPATCHAsync(id, MediaTypeNames.Application.Json, leadSObject); - /// - /// Method for get entity by external ID is not generated by BETA OpenApi 3 definition - /// Could be better solution when this endpoint will be generated in - /// - /// - /// - /// - public async Task GetLeadIdByExternalId(string fieldName, string externalId) - { - var apiVersion = integrationSettings.Value.ApiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); - using var request = - new HttpRequestMessage(HttpMethod.Get, - $"/services/data/v{apiVersion}/sobjects/lead/{fieldName}/{externalId}?fields=Id"); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); - var response = await httpClient.SendAsync(request); - - if (response.IsSuccessStatusCode) - { - var lead = await response.Content.ReadFromJsonAsync(); - return lead?.Id; - } - else if (response.StatusCode == HttpStatusCode.NotFound) - { - logger.LogWarning("Lead not found for external field name: '{ExternalFieldName}' and value: '{ExternalId}'", - fieldName, externalId); - return null; - } - else - { - string responseMessage = await response.Content.ReadAsStringAsync(); - throw new ApiException("Unexpected response", (int)response.StatusCode, responseMessage, null!, null); - } - } public async Task GetLeadById(string id, string? fields = null) => await apiClient.LeadGET2Async(id, fields); public async Task GetLeadByEmail(string email) + => await GetEntityIdByEmail(email, "Lead"); + + public async Task CreateContactAsync(ContactSObject contact) + => await apiClient.ContactPOSTAsync(MediaTypeNames.Application.Json, contact); + + public async Task UpdateContactAsync(string id, ContactSObject contact) + => await apiClient.ContactPATCHAsync(id, MediaTypeNames.Application.Json, contact); + + public async Task GetContactById(string id, string? fields = null) + => await apiClient.ContactGET2Async(id, fields); + + public async Task GetContactByEmail(string email) + => await GetEntityIdByEmail(email, "Contact"); + + private async Task GetEntityIdByEmail(string email, string entityName) { var apiVersion = integrationSettings.Value.ApiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); using var request = new HttpRequestMessage(HttpMethod.Get, - $"/services/data/v{apiVersion}/query?q=SELECT+Id+FROM+Lead+WHERE+Email='{HttpUtility.UrlEncode(email)}'+ORDER+BY+CreatedDate+DESC"); + $"/services/data/v{apiVersion}/query?q=SELECT+Id+FROM+{entityName}+WHERE+Email='{HttpUtility.UrlEncode(email)}'+ORDER+BY+CreatedDate+DESC"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); var response = await httpClient.SendAsync(request); @@ -100,29 +79,4 @@ public async Task UpdateLeadAsync(string id, LeadSObject leadSObject) throw new ApiException("Unexpected response", (int)response.StatusCode, responseMessage, null!, null); } } - - public Task CreateContactAsync(ContactSObject contact) - { - throw new NotImplementedException(); - } - - public Task UpdateContactAsync(string id, ContactSObject contact) - { - throw new NotImplementedException(); - } - - public Task GetContactIdByExternalId(string fieldName, string externalId) - { - throw new NotImplementedException(); - } - - public Task GetContactById(string id, string? fields = null) - { - throw new NotImplementedException(); - } - - public Task GetContactByEmail(string email) - { - throw new NotImplementedException(); - } } \ No newline at end of file From 083239cdc12b3c40595c73388c2fec17600eea37 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Thu, 25 Jan 2024 17:40:55 +0100 Subject: [PATCH 09/26] contacts wip --- .../CommonIntegrationSettings.cs | 2 + .../Configuration/ContactMappingBuilder.cs | 4 +- .../ContactMappingConfiguration.cs | 1 - .../Converters/ICRMTypeConverter.cs | 6 + .../Mapping/ICRMTypeConverter.cs | 6 - .../Services/ICRMSyncItemService.cs | 3 +- .../Services/IContactsIntegrationService.cs | 14 +- .../Implementations/CRMSyncItemService.cs | 6 +- .../Workers/ContactsSyncQueueWorkerBase.cs | 7 +- .../DynamicsBizFormsMappingBuilder.cs | 1 + .../DynamicsContactMappingBuilder.cs | 52 +++++-- .../DynamicsContactMappingConfiguration.cs | 4 +- .../ContactToKenticoContactConverter.cs | 25 +++ .../FormContactMappingToLeadConverter.cs | 5 +- .../LeadToKenticoContactConverter.cs | 26 ++++ .../DynamicsIntegrationGlobalEvents.cs | 2 +- .../DynamicsServiceCollectionExtensions.cs | 9 +- .../DynamicsContactsIntegrationService.cs | 145 +++++++++++++++--- .../DynamicsLeadsIntegrationService.cs | 1 + .../IDynamicsContactsIntegrationService.cs | 2 - .../Workers/ContactsSyncFromCRMWorker.cs | 27 +++- .../FormContactMappingToLeadConverter.cs | 5 +- .../ISalesForceContactsIntegrationService.cs | 2 - .../SalesForceContactMappingBuilder.cs | 6 +- .../SalesForceContactsIntegrationService.cs | 27 +++- .../SalesforceBizFormsMappingBuilder.cs | 1 + .../SalesforceLeadsIntegrationService.cs | 1 + 27 files changed, 306 insertions(+), 84 deletions(-) create mode 100644 src/Kentico.Xperience.CRM.Common/Converters/ICRMTypeConverter.cs delete mode 100644 src/Kentico.Xperience.CRM.Common/Mapping/ICRMTypeConverter.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Converters/ContactToKenticoContactConverter.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Converters/LeadToKenticoContactConverter.cs diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs index 9a0d71a..331e627 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs @@ -19,6 +19,8 @@ namespace Kentico.Xperience.CRM.Common.Configuration; /// public bool ContactsEnabled { get; set; } + public bool ContactsTwoWaySyncEnabled { get; set; } + /// /// Where to sync contact - Leads/Contacts /// diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs index 6e0f6cb..d0976fd 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs @@ -37,9 +37,9 @@ public TBuilder MapField(Func mappingFunc, string crmFieldN /// /// public TBuilder AddCustomValidation() - where TService : class, ILeadsIntegrationValidationService + where TService : class, IContactsIntegrationValidationService { - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); return (TBuilder)this; } diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs index d259a9d..e40ba5d 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs @@ -5,5 +5,4 @@ namespace Kentico.Xperience.CRM.Common.Configuration; public class ContactMappingConfiguration { public List FieldsMapping { get; init; } = new(); - public List Converters { get; init; } = new(); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Converters/ICRMTypeConverter.cs b/src/Kentico.Xperience.CRM.Common/Converters/ICRMTypeConverter.cs new file mode 100644 index 0000000..5bbed67 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Converters/ICRMTypeConverter.cs @@ -0,0 +1,6 @@ +namespace Kentico.Xperience.CRM.Common.Converters; + +public interface ICRMTypeConverter +{ + Task Convert(TSource source, TDestination destination); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Mapping/ICRMTypeConverter.cs b/src/Kentico.Xperience.CRM.Common/Mapping/ICRMTypeConverter.cs deleted file mode 100644 index b8e6dee..0000000 --- a/src/Kentico.Xperience.CRM.Common/Mapping/ICRMTypeConverter.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Kentico.Xperience.CRM.Common.Mapping; - -public interface ICRMTypeConverter -{ - Task Convert(TSource source, TDestination destination); -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs index 2cab81f..fa2a0cf 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs @@ -10,7 +10,6 @@ public interface ICRMSyncItemService Task LogFormLeadUpdateItem(BizFormItem bizFormItem, string crmId, string crmName); Task GetFormLeadSyncItem(BizFormItem bizFormItem, string crmName); - Task LogContactCreateItem(ContactInfo contactInfo, string crmId, string crmName); - Task LogContactUpdateItem(ContactInfo contactInfo, string crmId, string crmName); + Task LogContactSyncItem(ContactInfo contactInfo, string crmId, string crmName); Task GetContactSyncItem(ContactInfo contactInfo, string crmName); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationService.cs index 661c035..a56b02d 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/IContactsIntegrationService.cs @@ -12,9 +12,21 @@ public interface IContactsIntegrationService Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo); /// - /// + /// Creates contact in CRM from Contact info /// /// /// Task SynchronizeContactToContactsAsync(ContactInfo contactInfo); + + /// + /// Creates or updates contact info from CRM lead + /// + /// + Task SynchronizeLeadsToKenticoAsync(); + + /// + /// Creates or updates contact info from CRM contact + /// + /// + Task SynchronizeContactsToKenticoAsync(); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs index d9aee08..522deb2 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs @@ -36,7 +36,6 @@ private async Task LogFormLeadSyncItem(BizFormItem bizFormItem, string crmId, st else { syncItem.CRMSyncItemCRMID = crmId; - syncItem.CRMSyncItemCreatedByKentico = createdByKentico; syncItem.Update(); } } @@ -50,10 +49,7 @@ private async Task LogFormLeadSyncItem(BizFormItem bizFormItem, string crmId, st .GetEnumerableTypedResultAsync()) .FirstOrDefault(); - public Task LogContactCreateItem(ContactInfo contactInfo, string crmId, string crmName) - => LogContactSyncItem(contactInfo, crmId, crmName, createdByKentico: true); - - public Task LogContactUpdateItem(ContactInfo contactInfo, string crmId, string crmName) + public Task LogContactSyncItem(ContactInfo contactInfo, string crmId, string crmName) => LogContactSyncItem(contactInfo, crmId, crmName, createdByKentico: false); private async Task LogContactSyncItem(ContactInfo contactInfo, string crmId, string crmName, bool createdByKentico) diff --git a/src/Kentico.Xperience.CRM.Common/Workers/ContactsSyncQueueWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Workers/ContactsSyncQueueWorkerBase.cs index 896a13a..da68ddc 100644 --- a/src/Kentico.Xperience.CRM.Common/Workers/ContactsSyncQueueWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Workers/ContactsSyncQueueWorkerBase.cs @@ -33,14 +33,15 @@ protected override int ProcessItems(IEnumerable contacts) Debug.WriteLine($"Worker {GetType().FullName} running"); var failedSyncItemsService = Service.Resolve(); int processed = 0; - var contactList = contacts.ToList(); - var settings = Service.Resolve>().CurrentValue; - if (!settings.ContactsEnabled || !contactList.Any()) return 0; try { using (var serviceScope = Service.Resolve().CreateScope()) { + var contactList = contacts.ToList(); + var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; + if (!settings.ContactsEnabled || !contactList.Any()) return 0; + var contactsIntegrationService = serviceScope.ServiceProvider .GetRequiredService(); diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs index 7322bf0..18cd5b8 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs @@ -1,5 +1,6 @@ using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.Dynamics.Converters; diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingBuilder.cs index 6a2a00e..a96d492 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingBuilder.cs @@ -2,7 +2,9 @@ using CMS.Globalization; using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; +using Kentico.Xperience.CRM.Dynamics.Converters; using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; using Kentico.Xperience.CRM.Dynamics.Helpers; using Microsoft.Extensions.DependencyInjection; @@ -59,9 +61,9 @@ public DynamicsContactMappingBuilder AddDefaultMappingForLead() MapField(c => c.ContactCity, l => l.Address1_City); MapField(c => c.ContactZIP, l => l.Address1_PostalCode); MapField( - c => c.ContactCountryID > 0 ? - CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty : - string.Empty, l => l.Address1_Country); + c => c.ContactCountryID > 0 + ? CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty + : string.Empty, l => l.Address1_Country); MapField(c => c.ContactJobTitle, l => l.JobTitle); MapField(c => c.ContactMobilePhone, l => l.MobilePhone); MapField(c => c.ContactBusinessPhone, l => l.Telephone1); @@ -81,9 +83,9 @@ public DynamicsContactMappingBuilder AddDefaultMappingForContact() MapField(c => c.ContactCity, l => l.Address1_City); MapField(c => c.ContactZIP, l => l.Address1_PostalCode); MapField( - c => c.ContactCountryID > 0 ? - CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty : - string.Empty, l => l.Address1_Country); + c => c.ContactCountryID > 0 + ? CountryInfo.Provider.Get(c.ContactCountryID)?.CountryDisplayName ?? string.Empty + : string.Empty, l => l.Address1_Country); MapField(c => c.ContactJobTitle, l => l.JobTitle); MapField(c => c.ContactMobilePhone, l => l.MobilePhone); MapField(c => c.ContactBusinessPhone, l => l.Telephone1); @@ -95,25 +97,47 @@ public DynamicsContactMappingBuilder AddDefaultMappingForContact() public DynamicsContactMappingBuilder AddContactToLeadConverter() where TConverter : class, ICRMTypeConverter { - converters.Add(typeof(TConverter)); serviceCollection.TryAddEnumerable(ServiceDescriptor .Scoped, TConverter>()); return this; } - + public DynamicsContactMappingBuilder AddContactToContactConverter() where TConverter : class, ICRMTypeConverter { - converters.Add(typeof(TConverter)); serviceCollection.TryAddEnumerable(ServiceDescriptor .Scoped, TConverter>()); return this; } + public DynamicsContactMappingBuilder AddDefaultMappingToKenticoContact() + { + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, LeadToKenticoContactConverter>()); + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, ContactToKenticoContactConverter>()); + + return this; + } + + public DynamicsContactMappingBuilder AddLeadToKenticoConverter() + where TConverter : class, ICRMTypeConverter + { + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, TConverter>()); + + return this; + } + + public DynamicsContactMappingBuilder AddContactToKenticoConverter() + where TConverter : class, ICRMTypeConverter + { + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, TConverter>()); + + return this; + } + public DynamicsContactMappingConfiguration Build() => - new() - { - FieldsMapping = fieldMappings, - Converters = converters - }; + new() { FieldsMapping = fieldMappings }; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingConfiguration.cs index 59391b4..ab0113f 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsContactMappingConfiguration.cs @@ -2,6 +2,8 @@ namespace Kentico.Xperience.CRM.Dynamics.Configuration; +#pragma warning disable S2094 // Classes should not be empty public class DynamicsContactMappingConfiguration : ContactMappingConfiguration +#pragma warning restore S2094 // Classes should not be empty { -} \ No newline at end of file +} diff --git a/src/Kentico.Xperience.CRM.Dynamics/Converters/ContactToKenticoContactConverter.cs b/src/Kentico.Xperience.CRM.Dynamics/Converters/ContactToKenticoContactConverter.cs new file mode 100644 index 0000000..1ece59e --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Converters/ContactToKenticoContactConverter.cs @@ -0,0 +1,25 @@ +using CMS.ContactManagement; +using Kentico.Xperience.CRM.Common.Converters; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; + +namespace Kentico.Xperience.CRM.Dynamics.Converters; + +public class ContactToKenticoContactConverter : ICRMTypeConverter +{ + public Task Convert(Contact source, ContactInfo destination) + { + destination.ContactFirstName = source.FirstName; + destination.ContactMiddleName = source.MiddleName; + destination.ContactLastName = source.LastName; + destination.ContactJobTitle = source.JobTitle; + destination.ContactAddress1 = source.Address1_Line1; + destination.ContactCity = source.Address1_City; + destination.ContactZIP = source.Address1_PostalCode; + destination.ContactMobilePhone = source.MobilePhone; + destination.ContactBusinessPhone = source.Telephone1; + destination.ContactEmail = source.EMailAddress1; + destination.ContactNotes = source.Description; + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs b/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs index f29eda7..312197d 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs @@ -4,6 +4,7 @@ using CMS.Globalization; using CMS.OnlineForms; using CMS.OnlineForms.Internal; +using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; @@ -31,7 +32,7 @@ public FormContactMappingToLeadConverter( this.conversion = conversion; } - public Task Convert(BizFormItem source, Lead destination) + public Task Convert(BizFormItem source, Lead destination) { var firstName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactFirstName)); if (!string.IsNullOrWhiteSpace(firstName)) @@ -113,6 +114,6 @@ public Task Convert(BizFormItem source, Lead destination) destination.Address1_StateOrProvince = state?.StateDisplayName; } - return Task.FromResult(destination); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Converters/LeadToKenticoContactConverter.cs b/src/Kentico.Xperience.CRM.Dynamics/Converters/LeadToKenticoContactConverter.cs new file mode 100644 index 0000000..e9702e3 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Converters/LeadToKenticoContactConverter.cs @@ -0,0 +1,26 @@ +using CMS.ContactManagement; +using Kentico.Xperience.CRM.Common.Converters; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; + +namespace Kentico.Xperience.CRM.Dynamics.Converters; + +public class LeadToKenticoContactConverter : ICRMTypeConverter +{ + public Task Convert(Lead source, ContactInfo destination) + { + destination.ContactFirstName = source.FirstName; + destination.ContactMiddleName = source.MiddleName; + destination.ContactLastName = source.LastName; + destination.ContactJobTitle = source.JobTitle; + destination.ContactAddress1 = source.Address1_Line1; + destination.ContactCity = source.Address1_City; + destination.ContactZIP = source.Address1_PostalCode; + destination.ContactMobilePhone = source.MobilePhone; + destination.ContactBusinessPhone = source.Telephone1; + destination.ContactEmail = source.EMailAddress1; + destination.ContactNotes = source.Description; + destination.ContactCompanyName = source.CompanyName; + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs index c31ca94..9e995f3 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -91,7 +91,7 @@ private void ContactSync(object? sender, ObjectEventArgs args) { if (args.Object is not ContactInfo contactInfo || !ValidationHelper.IsEmail(contactInfo.ContactEmail)) return; - + ContactsSyncQueueWorker.Current.Enqueue(contactInfo); } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index c766bbd..05b1ded 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -53,12 +53,12 @@ public static IServiceCollection AddKenticoCRMDynamicsContactsIntegration(this I public static IServiceCollection AddKenticoCRMDynamicsContactsIntegration(this IServiceCollection serviceCollection, ContactCRMType crmType, Action mappingConfig, IConfiguration? configuration = null, - bool useDefaultMapping = true) + bool useDefaultMappingToCRM = true, bool useDefaultMappingToKentico = true) { serviceCollection.AddKenticoCrmCommonContactIntegration(); var mappingBuilder = new DynamicsContactMappingBuilder(serviceCollection); - if (useDefaultMapping) + if (useDefaultMappingToCRM) { mappingBuilder = crmType == ContactCRMType.Lead ? mappingBuilder.AddDefaultMappingForLead() : @@ -66,6 +66,11 @@ public static IServiceCollection AddKenticoCRMDynamicsContactsIntegration(this I mappingConfig(mappingBuilder); } + if (useDefaultMappingToKentico) + { + //@TODO + } + serviceCollection.TryAddSingleton( _ => mappingBuilder.Build()); diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs index b30bf69..549a538 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsContactsIntegrationService.cs @@ -1,5 +1,6 @@ using CMS.ContactManagement; using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; @@ -25,6 +26,9 @@ public class DynamicsContactsIntegrationService : IDynamicsContactsIntegrationSe private readonly IOptionsSnapshot settings; private readonly IEnumerable> contactLeadConverters; private readonly IEnumerable> contactContactConverters; + private readonly IEnumerable> leadKenticoConverters; + private readonly IEnumerable> contactKenticoConverters; + private readonly IContactInfoProvider contactInfoProvider; public DynamicsContactsIntegrationService(DynamicsContactMappingConfiguration contactMapping, IContactsIntegrationValidationService validationService, @@ -34,7 +38,10 @@ public DynamicsContactsIntegrationService(DynamicsContactMappingConfiguration co IFailedSyncItemService failedSyncItemService, IOptionsSnapshot settings, IEnumerable> contactLeadConverters, - IEnumerable> contactContactConverters) + IEnumerable> contactContactConverters, + IEnumerable> leadKenticoConverters, + IEnumerable> contactKenticoConverters, + IContactInfoProvider contactInfoProvider) { this.contactMapping = contactMapping; this.validationService = validationService; @@ -45,6 +52,9 @@ public DynamicsContactsIntegrationService(DynamicsContactMappingConfiguration co this.settings = settings; this.contactLeadConverters = contactLeadConverters; this.contactContactConverters = contactContactConverters; + this.leadKenticoConverters = leadKenticoConverters; + this.contactKenticoConverters = contactKenticoConverters; + this.contactInfoProvider = contactInfoProvider; } public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) @@ -66,7 +76,8 @@ public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) } else { - var existingLead = await GetEntityById(Guid.Parse(syncItem.CRMSyncItemCRMID), Lead.EntityLogicalName); + var existingLead = + await GetEntityById(Guid.Parse(syncItem.CRMSyncItemCRMID), Lead.EntityLogicalName); if (existingLead is null) { await UpdateLeadByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); @@ -117,7 +128,8 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) } else { - var existingContact = await GetEntityById(Guid.Parse(syncItem.CRMSyncItemCRMID), Contact.EntityLogicalName); + var existingContact = + await GetEntityById(Guid.Parse(syncItem.CRMSyncItemCRMID), Contact.EntityLogicalName); if (existingContact is null) { await UpdateContactByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); @@ -148,7 +160,7 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Dynamics); } } - + private async Task UpdateLeadByEmailOrCreate(ContactInfo contactInfo, IEnumerable fieldMappings) { @@ -174,7 +186,7 @@ private async Task UpdateLeadByEmailOrCreate(ContactInfo contactInfo, logger.LogInformation("Contact {ContactEmail} ignored", contactInfo.ContactEmail); } } - + private async Task UpdateContactByEmailOrCreate(ContactInfo contactInfo, IEnumerable fieldMappings) { @@ -210,7 +222,7 @@ private async Task CreateLeadAsync(ContactInfo contactInfo, IEnumerable fieldMappings) { var contactEntity = new Contact(); @@ -238,7 +250,7 @@ private async Task CreateContactAsync(ContactInfo contactInfo, IEnumerable> GetModifiedLeadsAsync(DateTime lastSync) + private async Task> GetModifiedLeadsAsync(DateTime lastSync) { return await GetModifiedEntitiesAsync(lastSync, Lead.EntityLogicalName); } - public async Task> GetModifiedContactsAsync(DateTime lastSync) + private async Task> GetModifiedContactsAsync(DateTime lastSync) { return await GetModifiedEntitiesAsync(lastSync, Contact.EntityLogicalName); } @@ -305,22 +317,119 @@ public async Task> GetModifiedContactsAsync(DateTime lastSy private async Task> GetModifiedEntitiesAsync(DateTime lastSync, string entityName) where TEntity : Entity { - var query = new QueryExpression(entityName) { ColumnSet = new ColumnSet(true) }; - query.Criteria.AddCondition("modifiedon", ConditionOperator.GreaterThan, lastSync.ToUniversalTime()); + try + { + var query = new QueryExpression(entityName) { ColumnSet = new ColumnSet(true) }; + query.Criteria.AddCondition("modifiedon", ConditionOperator.GreaterThan, lastSync.ToUniversalTime()); - return (await serviceClient.RetrieveMultipleAsync(query)).Entities.Select(e => e.ToEntity()); + return (await serviceClient.RetrieveMultipleAsync(query)).Entities.Select(e => e.ToEntity()) + .ToList(); + } + catch (FaultException e) + { + logger.LogError(e, "Get modified entities - api error: {ApiResult}", e.Detail); + return Enumerable.Empty(); + } + catch (Exception e) when (e.InnerException is FaultException ie) + { + logger.LogError(e, "Get modified entities - api error: {ApiResult}", ie.Detail); + return Enumerable.Empty(); + } + catch (Exception e) + { + logger.LogError(e, "Get modified entities - unknown api error"); + return Enumerable.Empty(); + } } private async Task GetEntityById(Guid leadId, string logicalName) - where TEntity : Entity + where TEntity : Entity => (await serviceClient.RetrieveAsync(logicalName, leadId, new ColumnSet(true)))?.ToEntity(); private async Task GetLeadByEmail(string email, string logicalName) - where TEntity : Entity + where TEntity : Entity { var query = new QueryExpression(Lead.EntityLogicalName) { ColumnSet = new ColumnSet(true), TopCount = 1 }; query.Criteria.AddCondition("emailaddress1", ConditionOperator.Equal, email); return (await serviceClient.RetrieveMultipleAsync(query)).Entities.FirstOrDefault()?.ToEntity(); } -} \ No newline at end of file + + public async Task SynchronizeLeadsToKenticoAsync() + { + var leads = await GetModifiedLeadsAsync(DateTime.UtcNow.AddMinutes(-1)); + foreach (var lead in leads) + { + try + { + if (string.IsNullOrWhiteSpace(lead.EMailAddress1)) + { + continue; + } + + var contactInfo = (await contactInfoProvider.Get() + .WhereEquals(nameof(ContactInfo.ContactEmail), lead.EMailAddress1) + .TopN(1) + .GetEnumerableTypedResultAsync())?.FirstOrDefault(); + + if (contactInfo is null) + { + contactInfo = new ContactInfo(); + } + + foreach (var converter in leadKenticoConverters) + { + await converter.Convert(lead, contactInfo); + } + + contactInfoProvider.Set(contactInfo); + + await syncItemService.LogContactSyncItem(contactInfo, lead.Id.ToString(), CRMType.Dynamics); + } + catch (Exception e) + { + logger.LogError(e, "Syncing to contact info {ContactEmail} failed", lead.EMailAddress1); + } + } + } + + public async Task SynchronizeContactsToKenticoAsync() + { + var contacts = await GetModifiedContactsAsync(DateTime.UtcNow.AddMinutes(-1)); + foreach (var contact in contacts) + { + try + { + if (string.IsNullOrWhiteSpace(contact.EMailAddress1)) + { + continue; + } + + var contactInfo = (await contactInfoProvider.Get() + .WhereEquals(nameof(ContactInfo.ContactEmail), contact.EMailAddress1) + .TopN(1) + .GetEnumerableTypedResultAsync())?.FirstOrDefault(); + + if (contactInfo is null) + { + contactInfo = new ContactInfo(); + } + + foreach (var converter in contactKenticoConverters) + { + await converter.Convert(contact, contactInfo); + } + + contactInfoProvider.Set(contactInfo); + + await syncItemService.LogContactSyncItem(contactInfo, contact.Id.ToString(), CRMType.Dynamics); + } + catch (Exception e) + { + logger.LogError(e, "Syncing to contact info {ContactEmail} failed", contact.EMailAddress1); + } + } + } +} + + diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs index d826654..0153c72 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs @@ -1,5 +1,6 @@ using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/IDynamicsContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/IDynamicsContactsIntegrationService.cs index bca04b5..0e21299 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/IDynamicsContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/IDynamicsContactsIntegrationService.cs @@ -5,6 +5,4 @@ namespace Kentico.Xperience.CRM.Dynamics.Services; public interface IDynamicsContactsIntegrationService : IContactsIntegrationService { - Task> GetModifiedLeadsAsync(DateTime lastSync); - Task> GetModifiedContactsAsync(DateTime lastSync); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncFromCRMWorker.cs b/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncFromCRMWorker.cs index 2dd5e5b..543b571 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncFromCRMWorker.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Workers/ContactsSyncFromCRMWorker.cs @@ -1,5 +1,6 @@ using CMS.Base; using CMS.Core; +using Kentico.Xperience.CRM.Common.Enums; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Services; using Microsoft.Extensions.DependencyInjection; @@ -17,20 +18,30 @@ public class ContactsSyncFromCRMWorker : ThreadWorker protected override void Process() { Debug.WriteLine($"Worker {GetType().FullName} running"); - - var settings = Service.Resolve>().CurrentValue; - if (!settings.ContactsEnabled) return; - + try { using (var scope = Service.Resolve().CreateScope()) { + var settings = scope.ServiceProvider.GetRequiredService>().Value; + if (!settings.ContactsEnabled) return; + var contactsIntegrationService = scope.ServiceProvider.GetRequiredService(); - //@TODO - var contacts = contactsIntegrationService.GetModifiedLeadsAsync(DateTime.UtcNow.AddMinutes(-1)) - .GetAwaiter() - .GetResult(); + + if (settings.ContactType == ContactCRMType.Lead) + { + contactsIntegrationService.SynchronizeLeadsToKenticoAsync() + .GetAwaiter() + .GetResult(); + } + + if (settings.ContactType == ContactCRMType.Contact) + { + contactsIntegrationService.SynchronizeContactsToKenticoAsync() + .GetAwaiter() + .GetResult(); + } } } catch (Exception e) diff --git a/src/Kentico.Xperience.CRM.Salesforce/Converters/FormContactMappingToLeadConverter.cs b/src/Kentico.Xperience.CRM.Salesforce/Converters/FormContactMappingToLeadConverter.cs index a9a51a2..ccadcf1 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Converters/FormContactMappingToLeadConverter.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Converters/FormContactMappingToLeadConverter.cs @@ -4,6 +4,7 @@ using CMS.Globalization; using CMS.OnlineForms; using CMS.OnlineForms.Internal; +using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; using Salesforce.OpenApi; @@ -31,7 +32,7 @@ public FormContactMappingToLeadConverter( this.conversion = conversion; } - public Task Convert(BizFormItem source, LeadSObject destination) + public Task Convert(BizFormItem source, LeadSObject destination) { var firstName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactFirstName)); if (!string.IsNullOrWhiteSpace(firstName)) @@ -107,6 +108,6 @@ public Task Convert(BizFormItem source, LeadSObject destination) destination.State = state?.StateDisplayName; } - return Task.FromResult(destination); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Salesforce/ISalesForceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Salesforce/ISalesForceContactsIntegrationService.cs index 0b5a672..dbb7e0f 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/ISalesForceContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/ISalesForceContactsIntegrationService.cs @@ -5,6 +5,4 @@ namespace Kentico.Xperience.CRM.Salesforce.Services; public interface ISalesforceContactsIntegrationService : IContactsIntegrationService { - Task> GetModifiedLeadsAsync(DateTime lastSync); - Task> GetModifiedContactsAsync(DateTime lastSync); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Salesforce/SalesForceContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.Salesforce/SalesForceContactMappingBuilder.cs index 822179c..3cdc4a6 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/SalesForceContactMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/SalesForceContactMappingBuilder.cs @@ -1,6 +1,7 @@ using CMS.ContactManagement; using CMS.Globalization; using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; @@ -96,7 +97,6 @@ public SalesforceContactMappingBuilder AddDefaultMappingForContact() public SalesforceContactMappingBuilder AddContactToLeadConverter() where TConverter : class, ICRMTypeConverter { - converters.Add(typeof(TConverter)); serviceCollection.TryAddEnumerable(ServiceDescriptor .Scoped, TConverter>()); return this; @@ -105,7 +105,6 @@ public SalesforceContactMappingBuilder AddContactToLeadConverter() public SalesforceContactMappingBuilder AddContactToContactConverter() where TConverter : class, ICRMTypeConverter { - converters.Add(typeof(TConverter)); serviceCollection.TryAddEnumerable(ServiceDescriptor .Scoped, TConverter>()); return this; @@ -114,7 +113,6 @@ public SalesforceContactMappingBuilder AddContactToContactConverter( public SalesforceContactMappingConfiguration Build() => new() { - FieldsMapping = fieldMappings, - Converters = converters + FieldsMapping = fieldMappings }; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Salesforce/SalesForceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Salesforce/SalesForceContactsIntegrationService.cs index b101f22..20c4ef2 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/SalesForceContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/SalesForceContactsIntegrationService.cs @@ -1,5 +1,6 @@ using CMS.ContactManagement; using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; @@ -148,12 +149,22 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) } } - public Task> GetModifiedLeadsAsync(DateTime lastSync) + public Task SynchronizeLeadsToKenticoAsync() { throw new NotImplementedException(); } - public Task> GetModifiedContactsAsync(DateTime lastSync) + public Task SynchronizeContactsToKenticoAsync() + { + throw new NotImplementedException(); + } + + private Task> GetModifiedLeadsAsync(DateTime lastSync) + { + throw new NotImplementedException(); + } + + private Task> GetModifiedContactsAsync(DateTime lastSync) { throw new NotImplementedException(); } @@ -221,7 +232,7 @@ private async Task CreateLeadAsync(ContactInfo contactInfo, IEnumerable Date: Fri, 26 Jan 2024 19:22:57 +0100 Subject: [PATCH 10/26] contacts finished --- examples/DancingGoat/Program.cs | 6 +- examples/DancingGoat/packages.lock.json | 1 - .../Synchronization/CRMSyncItemService.cs | 6 +- .../Synchronization/CRMSyncQueueWorkerBase.cs | 101 +++++++++ .../ContactSyncFromCRMWorkerBase.cs | 59 ++++++ .../ContactsSyncQueueWorkerBase.cs | 79 ------- .../Synchronization/FailedSyncItemService.cs | 37 +++- .../FailedSyncItemsWorkerBase.cs | 49 ++++- .../Synchronization/ICRMSyncItemService.cs | 2 +- .../Synchronization/IFailedSyncItemService.cs | 21 +- .../ContactToKenticoContactConverter.cs | 1 + .../DynamicsIntegrationGlobalEvents.cs | 36 +--- .../DynamicsServiceCollectionExtensions.cs | 37 ++-- .../ContactsSyncFromCRMWorker.cs | 52 +---- .../DynamicsContactsIntegrationService.cs | 193 +++++++++--------- ...ueWorker.cs => DynamicsSyncQueueWorker.cs} | 5 +- .../Synchronization/FailedItemsWorker.cs | 1 + .../ContactToKenticoContactConverter.cs | 23 +++ .../LeadToKenticoContactConverter.cs | 24 +++ .../SalesforceContactMappingBuilder.cs | 37 +++- .../ContactsSyncFromCRMWorker.cs | 12 ++ .../SalesforceContactsIntegrationService.cs | 148 ++++++++++---- ...Worker.cs => SalesforceSyncQueueWorker.cs} | 5 +- .../Kentico.Xperience.CRM.Salesforce.csproj | 1 - .../SalesforceIntegrationGlobalEvents.cs | 39 ++-- .../SalesforceServiceCollectionsExtensions.cs | 50 +++-- .../Synchronization/FailedItemsWorker.cs | 2 +- .../Synchronization/ISalesforceApiService.cs | 17 +- .../Synchronization/SalesforceApiService.cs | 32 ++- 29 files changed, 691 insertions(+), 385 deletions(-) create mode 100644 src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncQueueWorkerBase.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs delete mode 100644 src/Kentico.Xperience.CRM.Common/Synchronization/ContactsSyncQueueWorkerBase.cs rename src/Kentico.Xperience.CRM.Dynamics/Synchronization/{ContactsSyncQueueWorker.cs => DynamicsSyncQueueWorker.cs} (55%) create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactToKenticoContactConverter.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Configuration/LeadToKenticoContactConverter.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Synchronization/ContactsSyncFromCRMWorker.cs rename src/Kentico.Xperience.CRM.SalesForce/Synchronization/{ContactsSyncQueueWorker.cs => SalesforceSyncQueueWorker.cs} (54%) diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index 6cfd5fb..381e7c7 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -93,11 +93,9 @@ // builder.Services.AddDynamicsContactsIntegration(ContactCRMType.Lead, // builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); -builder.Services.AddKenticoCRMDynamicsContactsIntegration(ContactCRMType.Contact, - builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); +builder.Services.AddKenticoCRMDynamicsContactsIntegration(ContactCRMType.Contact); -builder.Services.AddKenticoCRMSalesforceContactsIntegration(ContactCRMType.Lead, - builder.Configuration.GetSection(SalesforceIntegrationSettings.ConfigKeyName)); +builder.Services.AddKenticoCRMSalesforceContactsIntegration(ContactCRMType.Lead); //CRM integration registration end var app = builder.Build(); diff --git a/examples/DancingGoat/packages.lock.json b/examples/DancingGoat/packages.lock.json index 9ded4d0..5c6cc84 100644 --- a/examples/DancingGoat/packages.lock.json +++ b/examples/DancingGoat/packages.lock.json @@ -1407,7 +1407,6 @@ "Duende.AccessTokenManagement.OpenIdConnect": "[2.1.0, )", "IdentityModel": "[6.2.0, )", "Kentico.Xperience.CRM.Common": "[1.0.0-prerelease-1, )", - "Kentico.Xperience.CRM.Dynamics": "[1.0.0-prerelease-1, )", "Kentico.Xperience.Core": "[28.0.0, )" } }, diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncItemService.cs index 9def12c..e36cbf3 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncItemService.cs @@ -47,11 +47,9 @@ private async Task LogFormLeadSyncItem(BizFormItem bizFormItem, string crmId, st .WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityCRM), crmName) .GetEnumerableTypedResultAsync()) .FirstOrDefault(); - - public Task LogContactSyncItem(ContactInfo contactInfo, string crmId, string crmName) - => LogContactSyncItem(contactInfo, crmId, crmName, createdByKentico: false); - private async Task LogContactSyncItem(ContactInfo contactInfo, string crmId, string crmName, bool createdByKentico) + public async Task LogContactSyncItem(ContactInfo contactInfo, string crmId, string crmName, + bool createdByKentico = false) { var syncItem = await GetContactSyncItem(contactInfo, crmName); if (syncItem is null) diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncQueueWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncQueueWorkerBase.cs new file mode 100644 index 0000000..755e0a8 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncQueueWorkerBase.cs @@ -0,0 +1,101 @@ +using CMS.Base; +using CMS.ContactManagement; +using CMS.Core; +using CMS.DataEngine; +using CMS.OnlineForms; +using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Enums; +using Kentico.Xperience.CRM.Common.Services; +using Kentico.Xperience.CRM.Common.Synchronization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Diagnostics; + +namespace Kentico.Xperience.CRM.Common.Workers; + +public abstract class + CRMSyncQueueWorkerBase : ThreadQueueWorker + where TWorker : ThreadQueueWorker, new() + where TFormLeadsService : ILeadsIntegrationService + where TContactsService : IContactsIntegrationService + where TSettings : CommonIntegrationSettings + where TApiConfig : new() +{ + private readonly ILogger logger = Service.Resolve>(); + + /// + protected override int DefaultInterval => 10000; + + /// + protected override void ProcessItem(BaseInfo item) + { + } + + /// + protected override int ProcessItems(IEnumerable items) + { + Debug.WriteLine($"Worker {GetType().FullName} running"); + + var itemsList = items.ToList(); + var formItems = itemsList.OfType().ToList(); + var contactList = itemsList.OfType().ToList(); + + var failedSyncItemsService = Service.Resolve(); + + using (var serviceScope = Service.Resolve().CreateScope()) + { + var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; + + if (settings.FormLeadsEnabled && formItems.Any()) + { + try + { + var leadsIntegrationService = serviceScope.ServiceProvider + .GetRequiredService(); + + foreach (var formItem in formItems) + { + leadsIntegrationService.SynchronizeLeadAsync(formItem).GetAwaiter().GetResult(); + } + } + catch (Exception exception) + { + logger.LogError(exception, "Error occured during updating lead"); + failedSyncItemsService.LogFailedLeadItems(formItems, CRMName); + } + } + + if (settings.ContactsEnabled && contactList.Any()) + { + try + { + var contactsIntegrationService = serviceScope.ServiceProvider + .GetRequiredService(); + + foreach (var contactInfo in contactList) + { + (settings.ContactType == ContactCRMType.Lead ? + contactsIntegrationService.SynchronizeContactToLeadsAsync(contactInfo) : + contactsIntegrationService.SynchronizeContactToContactsAsync(contactInfo)) + .GetAwaiter().GetResult(); + } + } + catch (Exception exception) + { + logger.LogError(exception, "Error occured during contacts sync"); + failedSyncItemsService.LogFailedContactItems(contactList, CRMName); + } + } + } + + return formItems.Count + contactList.Count; + } + + /// + protected override void Finish() => RunProcess(); + + protected abstract string CRMName { get; } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs new file mode 100644 index 0000000..20b3ee3 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs @@ -0,0 +1,59 @@ +using CMS.Base; +using CMS.Core; +using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Enums; +using Kentico.Xperience.CRM.Common.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Diagnostics; + +namespace Kentico.Xperience.CRM.Common.Synchronization; + +public abstract class + ContactSyncFromCRMWorkerBase : ThreadWorker + where TWorker : ThreadWorker, new() + where TContactsService : IContactsIntegrationService + where TSettings : CommonIntegrationSettings + where TApiConfig : new() +{ + protected override int DefaultInterval => 60000; + private ILogger logger = null!; + + protected abstract string CRMName { get; } + + protected override void Initialize() + { + base.Initialize(); + logger = Service.Resolve>(); + } + + protected override void Process() + { + Debug.WriteLine($"Worker {GetType().FullName} running"); + + try + { + using (var scope = Service.Resolve().CreateScope()) + { + var settings = scope.ServiceProvider.GetRequiredService>().Value; + //@TODO new setting for 2-way + if (!settings.ContactsEnabled) return; + + var contactsIntegrationService = + scope.ServiceProvider.GetRequiredService(); + + (settings.ContactType == ContactCRMType.Lead ? + contactsIntegrationService.SynchronizeLeadsToKenticoAsync() : + contactsIntegrationService.SynchronizeContactsToKenticoAsync()) + .GetAwaiter().GetResult(); + } + } + catch (Exception e) + { + logger.LogError(e, "Error occured during contacts sync"); + } + } + + protected override void Finish() { } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/ContactsSyncQueueWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/ContactsSyncQueueWorkerBase.cs deleted file mode 100644 index adad0da..0000000 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/ContactsSyncQueueWorkerBase.cs +++ /dev/null @@ -1,79 +0,0 @@ -using CMS.Base; -using CMS.ContactManagement; -using CMS.Core; -using Kentico.Xperience.CRM.Common.Configuration; -using Kentico.Xperience.CRM.Common.Enums; -using Kentico.Xperience.CRM.Common.Services; -using Kentico.Xperience.CRM.Common.Synchronization; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.Diagnostics; - -namespace Kentico.Xperience.CRM.Common.Workers; - -public abstract class ContactsSyncQueueWorkerBase : ThreadQueueWorker - where TWorker : ThreadQueueWorker, new() - where TService : IContactsIntegrationService - where TSettings : CommonIntegrationSettings - where TApiConfig : new() -{ - private readonly ILogger logger = Service.Resolve>(); - - /// - protected override int DefaultInterval => 10000; - - /// - protected override void ProcessItem(ContactInfo item) - { - } - - /// - protected override int ProcessItems(IEnumerable contacts) - { - Debug.WriteLine($"Worker {GetType().FullName} running"); - var failedSyncItemsService = Service.Resolve(); - int processed = 0; - - try - { - using (var serviceScope = Service.Resolve().CreateScope()) - { - var contactList = contacts.ToList(); - var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; - if (!settings.ContactsEnabled || !contactList.Any()) return 0; - - var contactsIntegrationService = serviceScope.ServiceProvider - .GetRequiredService(); - - foreach (var contact in contactList) - { - if (settings.ContactType == ContactCRMType.Lead) - { - contactsIntegrationService.SynchronizeContactToLeadsAsync(contact).ConfigureAwait(false) - .GetAwaiter().GetResult(); - } - else - { - contactsIntegrationService.SynchronizeContactToContactsAsync(contact).ConfigureAwait(false) - .GetAwaiter().GetResult(); - } - processed++; - } - } - } - catch (Exception exception) - { - logger.LogError(exception, "Error occured during contacts sync"); - //@TODO - //failedSyncItemsService.LogFailedContactItem(contactInfo, CRMName); - } - - return processed; - } - - /// - protected override void Finish() => RunProcess(); - - protected abstract string CRMName { get; } -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemService.cs index b9d584a..65a1f84 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemService.cs @@ -20,15 +20,37 @@ public FailedSyncItemService(IFailedSyncItemInfoProvider failedSyncItemInfoProvi } public void LogFailedLeadItem(BizFormItem bizFormItem, string crmName) + => LogFailedCRMItem(crmName, bizFormItem.BizFormClassName, bizFormItem.ItemID); + + public void LogFailedContactItem(ContactInfo contactInfo, string crmName) + => LogFailedCRMItem(crmName, contactInfo.ClassName, contactInfo.ContactID); + + public void LogFailedLeadItems(IEnumerable bizFormItems, string crmName) { - var existingItem = GetExistingItem(crmName, bizFormItem.BizFormClassName, bizFormItem.ItemID); + foreach (var item in bizFormItems) + { + LogFailedLeadItem(item, crmName); + } + } + + public void LogFailedContactItems(IEnumerable contactInfos, string crmName) + { + foreach (var contactInfo in contactInfos) + { + LogFailedContactItem(contactInfo, crmName); + } + } + + private void LogFailedCRMItem(string crmName, string entityClass, int entityId) + { + var existingItem = GetExistingItem(crmName, entityClass, entityId); if (existingItem is null) { existingItem = new FailedSyncItemInfo { - FailedSyncItemEntityClass = bizFormItem.BizFormClassName, - FailedSyncItemEntityID = bizFormItem.ItemID, + FailedSyncItemEntityClass = entityClass, + FailedSyncItemEntityID = entityId, FailedSyncItemEntityCRM = crmName, FailedSyncItemNextTime = DateTime.Now.AddMinutes(1), FailedSyncItemTryCount = 0 @@ -48,11 +70,6 @@ public void LogFailedLeadItem(BizFormItem bizFormItem, string crmName) failedSyncItemInfoProvider.Set(existingItem); } - public void LogFailedContactItem(ContactInfo contactInfo, string crmName) - { - throw new NotImplementedException(); - } - public IEnumerable GetFailedSyncItemsToReSync(string crmName) { return failedSyncItemInfoProvider.Get() @@ -76,9 +93,9 @@ public IEnumerable GetFailedSyncItemsToReSync(string crmName .FirstOrDefault(); } - public void DeleteFailedSyncItem(string crmCrmName, string entityClass, int entityId) + public void DeleteFailedSyncItem(string crmName, string entityClass, int entityId) { - GetExistingItem(crmCrmName, entityClass, entityId)?.Delete(); + GetExistingItem(crmName, entityClass, entityId)?.Delete(); } private FailedSyncItemInfo? GetExistingItem(string crmName, string entityClass, int entityId) diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemsWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemsWorkerBase.cs index 89bdda1..3183878 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemsWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemsWorkerBase.cs @@ -1,6 +1,9 @@ using CMS.Base; +using CMS.ContactManagement; using CMS.Core; using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Enums; +using Kentico.Xperience.CRM.Common.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -13,12 +16,15 @@ namespace Kentico.Xperience.CRM.Common.Synchronization; /// Concrete implementation for each CRM must exists /// /// -/// +/// /// /// -public abstract class FailedSyncItemsWorkerBase : ThreadWorker +public abstract class + FailedSyncItemsWorkerBase : ThreadWorker where TWorker : ThreadWorker, new() - where TService : ILeadsIntegrationService + where TFormLeadsService : ILeadsIntegrationService + where TContactsService : IContactsIntegrationService where TSettings : CommonIntegrationSettings where TApiConfig : new() { @@ -46,19 +52,42 @@ protected override void Process() var failedSyncItemsService = Service.Resolve(); ILeadsIntegrationService? leadsIntegrationService = null; + IContactsIntegrationService? contactsIntegrationService = null; foreach (var syncItem in failedSyncItemsService.GetFailedSyncItemsToReSync(CRMName)) { - leadsIntegrationService ??= serviceScope.ServiceProvider - .GetRequiredService(); - - var bizFormItem = failedSyncItemsService.GetBizFormItem(syncItem); - if (bizFormItem is null) + // contacts + if (syncItem.FailedSyncItemEntityClass == ContactInfo.TYPEINFO.ObjectClassName) { - continue; + contactsIntegrationService ??= serviceScope.ServiceProvider.GetRequiredService(); + + var contactInfo = ContactInfo.Provider.Get(syncItem.FailedSyncItemEntityID); + if (contactInfo is null) + { + syncItem.Delete(); + continue; + } + + (settings.ContactType == ContactCRMType.Lead ? + contactsIntegrationService.SynchronizeContactToLeadsAsync(contactInfo) : + contactsIntegrationService.SynchronizeContactToContactsAsync(contactInfo)) + .GetAwaiter().GetResult(); } + //form submissions + else + { + leadsIntegrationService ??= serviceScope.ServiceProvider + .GetRequiredService(); + + var bizFormItem = failedSyncItemsService.GetBizFormItem(syncItem); + if (bizFormItem is null) + { + syncItem.Delete(); + continue; + } - leadsIntegrationService.SynchronizeLeadAsync(bizFormItem).ConfigureAwait(false).GetAwaiter().GetResult(); + leadsIntegrationService.SynchronizeLeadAsync(bizFormItem).GetAwaiter().GetResult(); + } } } catch (Exception e) diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/ICRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/ICRMSyncItemService.cs index 7793c76..0da5be3 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/ICRMSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/ICRMSyncItemService.cs @@ -9,6 +9,6 @@ public interface ICRMSyncItemService Task LogFormLeadUpdateItem(BizFormItem bizFormItem, string crmId, string crmName); Task GetFormLeadSyncItem(BizFormItem bizFormItem, string crmName); - Task LogContactSyncItem(ContactInfo contactInfo, string crmId, string crmName); + Task LogContactSyncItem(ContactInfo contactInfo, string crmId, string crmName, bool createdByKentico = false); Task GetContactSyncItem(ContactInfo contactInfo, string crmName); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/IFailedSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/IFailedSyncItemService.cs index c6fc55b..9b030bc 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/IFailedSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/IFailedSyncItemService.cs @@ -24,6 +24,21 @@ public interface IFailedSyncItemService /// void LogFailedContactItem(ContactInfo contactInfo, string crmName); + /// + /// Creates new record in failed items table or increment TrySyncCount property when record exists. + /// Next sync time is planned. + /// + /// BizForm item + /// CRM name + void LogFailedLeadItems(IEnumerable bizFormItems, string crmName); + + /// + /// @TODO + /// + /// + /// + void LogFailedContactItems(IEnumerable contactInfos, string crmName); + /// /// Get all items waiting for synchronization which can be already synced again (according SyncNextTime property) /// @@ -37,12 +52,12 @@ public interface IFailedSyncItemService /// /// BizFormItem? GetBizFormItem(FailedSyncItemInfo failedSyncItemInfo); - + /// /// Delete record for given CRM, class name and ID /// - /// CRM name + /// CRM name /// Entity class /// Entity ID - void DeleteFailedSyncItem(string crmCrmName, string entityClass, int entityId); + void DeleteFailedSyncItem(string crmName, string entityClass, int entityId); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactToKenticoContactConverter.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactToKenticoContactConverter.cs index 1ece59e..5edae79 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactToKenticoContactConverter.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/ContactToKenticoContactConverter.cs @@ -19,6 +19,7 @@ public Task Convert(Contact source, ContactInfo destination) destination.ContactBusinessPhone = source.Telephone1; destination.ContactEmail = source.EMailAddress1; destination.ContactNotes = source.Description; + destination.ContactBirthday = source.BirthDate ?? DateTime.MinValue; return Task.CompletedTask; } diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs index 9bc7096..7a5dff7 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -7,13 +7,10 @@ using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Admin; using Kentico.Xperience.CRM.Common.Constants; -using Kentico.Xperience.CRM.Common.Synchronization; using Kentico.Xperience.CRM.Dynamics; -using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Synchronization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; [assembly: RegisterModule(typeof(DynamicsIntegrationGlobalEvents))] @@ -44,6 +41,7 @@ protected override void OnInit(ModuleInitParameters parameters) BizFormItemEvents.Insert.After += SynchronizeBizFormLead; BizFormItemEvents.Update.After += SynchronizeBizFormLead; + ContactInfo.TYPEINFO.Events.Insert.After += ContactSync; ContactInfo.TYPEINFO.Events.Update.After += ContactSync; @@ -52,7 +50,7 @@ protected override void OnInit(ModuleInitParameters parameters) RequestEvents.RunEndRequestTasks.Execute += (_, _) => { - ContactsSyncQueueWorker.Current.EnsureRunningThread(); + DynamicsSyncQueueWorker.Current.EnsureRunningThread(); ContactsSyncFromCRMWorker.Current.EnsureRunningThread(); FailedItemsWorker.Current.EnsureRunningThread(); }; @@ -62,35 +60,19 @@ private void InitializeModule(object? sender, EventArgs e) { installer?.Install(CRMType.Dynamics); } - + private void SynchronizeBizFormLead(object? sender, BizFormItemEventArgs e) { - var failedSyncItemsService = Service.Resolve(); - try - { - using (var serviceScope = Service.Resolve().CreateScope()) - { - var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; - if (!settings.FormLeadsEnabled) return; - - var leadsIntegrationService = serviceScope.ServiceProvider - .GetRequiredService(); - - leadsIntegrationService.SynchronizeLeadAsync(e.Item).ConfigureAwait(false).GetAwaiter().GetResult(); - } - } - catch (Exception exception) - { - logger.LogError(exception, "Error occured during updating lead"); - failedSyncItemsService.LogFailedLeadItem(e.Item, CRMType.Dynamics); - } + DynamicsSyncQueueWorker.Current.Enqueue(e.Item); } private void ContactSync(object? sender, ObjectEventArgs args) { - if (args.Object is not ContactInfo contactInfo || !ValidationHelper.IsEmail(contactInfo.ContactEmail)) + if (args.Object is not ContactInfo contactInfo || + ValidationHelper.GetBoolean(RequestStockHelper.GetItem("SuppressEvents"), false) || + !ValidationHelper.IsEmail(contactInfo.ContactEmail)) return; - - ContactsSyncQueueWorker.Current.Enqueue(contactInfo); + + DynamicsSyncQueueWorker.Current.Enqueue(contactInfo); } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index f73a8d4..5fd6bc5 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -46,16 +46,25 @@ public static IServiceCollection AddKenticoCRMDynamics(this IServiceCollection s return serviceCollection; } - public static IServiceCollection AddKenticoCRMDynamicsContactsIntegration(this IServiceCollection serviceCollection, - ContactCRMType crmType, IConfiguration configuration) - => serviceCollection.AddKenticoCRMDynamicsContactsIntegration(crmType, b => { }, configuration); - - public static IServiceCollection AddKenticoCRMDynamicsContactsIntegration(this IServiceCollection serviceCollection, - ContactCRMType crmType, Action mappingConfig, IConfiguration? configuration = null, - bool useDefaultMappingToCRM = true, bool useDefaultMappingToKentico = true) + public static IServiceCollection AddKenticoCRMDynamicsContactsIntegration( + this IServiceCollection serviceCollection, + ContactCRMType crmType, + IConfiguration? configuration = null, + bool useDefaultMappingToCRM = true, + bool useDefaultMappingToKentico = true) + => serviceCollection.AddKenticoCRMDynamicsContactsIntegration(crmType, b => { }, configuration, + useDefaultMappingToCRM, useDefaultMappingToKentico); + + public static IServiceCollection AddKenticoCRMDynamicsContactsIntegration( + this IServiceCollection serviceCollection, + ContactCRMType crmType, + Action mappingConfig, + IConfiguration? configuration = null, + bool useDefaultMappingToCRM = true, + bool useDefaultMappingToKentico = true) { serviceCollection.AddKenticoCrmCommonContactIntegration(); - + var mappingBuilder = new DynamicsContactMappingBuilder(serviceCollection); if (useDefaultMappingToCRM) { @@ -67,12 +76,12 @@ public static IServiceCollection AddKenticoCRMDynamicsContactsIntegration(this I if (useDefaultMappingToKentico) { - //@TODO + mappingBuilder.AddDefaultMappingToKenticoContact(); } serviceCollection.TryAddSingleton( _ => mappingBuilder.Build()); - + if (configuration is null) { serviceCollection.AddOptions() @@ -83,9 +92,8 @@ public static IServiceCollection AddKenticoCRMDynamicsContactsIntegration(this I { serviceCollection.AddOptions().Bind(configuration) .PostConfigure(s => s.ContactType = crmType); - } - + serviceCollection.TryAddSingleton(GetCrmServiceClient); serviceCollection.AddScoped(); @@ -115,12 +123,13 @@ private static ServiceClient GetCrmServiceClient(IServiceProvider serviceProvide return new ServiceClient(connectionString, logger); } - private static void ConfigureWithCMSSettings(DynamicsIntegrationSettings settings, ICRMSettingsService settingsService) + private static void ConfigureWithCMSSettings(DynamicsIntegrationSettings settings, + ICRMSettingsService settingsService) { var settingsInfo = settingsService.GetSettings(CRMType.Dynamics); settings.FormLeadsEnabled = settingsInfo?.CRMIntegrationSettingsFormsEnabled ?? false; settings.ContactsEnabled = settingsInfo?.CRMIntegrationSettingsContactsEnabled ?? false; - + settings.IgnoreExistingRecords = settingsInfo?.CRMIntegrationSettingsIgnoreExistingRecords ?? false; settings.ApiConfig.DynamicsUrl = settingsInfo?.CRMIntegrationSettingsUrl; diff --git a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/ContactsSyncFromCRMWorker.cs b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/ContactsSyncFromCRMWorker.cs index 367cf56..beb0e9e 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/ContactsSyncFromCRMWorker.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/ContactsSyncFromCRMWorker.cs @@ -1,53 +1,11 @@ -using CMS.Base; -using CMS.Core; -using Kentico.Xperience.CRM.Common.Enums; +using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Synchronization; using Kentico.Xperience.CRM.Dynamics.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.Diagnostics; namespace Kentico.Xperience.CRM.Dynamics.Synchronization; -public class ContactsSyncFromCRMWorker : ThreadWorker +internal class ContactsSyncFromCRMWorker : ContactSyncFromCRMWorkerBase { - private readonly ILogger logger = Service.Resolve>(); - protected override int DefaultInterval => 60000; - - protected override void Process() - { - Debug.WriteLine($"Worker {GetType().FullName} running"); - - try - { - using (var scope = Service.Resolve().CreateScope()) - { - var settings = scope.ServiceProvider.GetRequiredService>().Value; - if (!settings.ContactsEnabled) return; - - var contactsIntegrationService = - scope.ServiceProvider.GetRequiredService(); - - if (settings.ContactType == ContactCRMType.Lead) - { - contactsIntegrationService.SynchronizeLeadsToKenticoAsync() - .GetAwaiter() - .GetResult(); - } - - if (settings.ContactType == ContactCRMType.Contact) - { - contactsIntegrationService.SynchronizeContactsToKenticoAsync() - .GetAwaiter() - .GetResult(); - } - } - } - catch (Exception e) - { - logger.LogError(e, "Error occured during contacts sync"); - } - } - - protected override void Finish() { } + protected override string CRMName => CRMType.Dynamics; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs index 7abfa6e..40d4b02 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs @@ -1,4 +1,6 @@ -using CMS.ContactManagement; +using CMS.Base; +using CMS.ContactManagement; +using CMS.Helpers; using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; @@ -162,6 +164,91 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) } } + + public async Task SynchronizeLeadsToKenticoAsync() + { + RequestStockHelper.Add("SuppressEvents", true); + var leads = await GetModifiedLeadsAsync(DateTime.UtcNow.AddMinutes(-1)); + foreach (var lead in leads) + { + try + { + if (string.IsNullOrWhiteSpace(lead.EMailAddress1)) + { + continue; + } + + var contactInfo = (await contactInfoProvider.Get() + .WhereEquals(nameof(ContactInfo.ContactEmail), lead.EMailAddress1) + .TopN(1) + .GetEnumerableTypedResultAsync())?.FirstOrDefault(); + + if (contactInfo is null) + { + contactInfo = new ContactInfo(); + } + + foreach (var converter in leadKenticoConverters) + { + await converter.Convert(lead, contactInfo); + } + + + if (contactInfo.HasChanged) + { + contactInfoProvider.Set(contactInfo); + await syncItemService.LogContactSyncItem(contactInfo, lead.Id.ToString(), CRMType.Dynamics); + } + } + catch (Exception e) + { + logger.LogError(e, "Syncing to contact info {ContactEmail} failed", lead.EMailAddress1); + } + } + } + + public async Task SynchronizeContactsToKenticoAsync() + { + RequestStockHelper.Add("SuppressEvents", true); + var contacts = await GetModifiedContactsAsync(DateTime.UtcNow.AddMinutes(-1)); + foreach (var contact in contacts) + { + try + { + if (string.IsNullOrWhiteSpace(contact.EMailAddress1)) + { + continue; + } + + var contactInfo = (await contactInfoProvider.Get() + .WhereEquals(nameof(ContactInfo.ContactEmail), contact.EMailAddress1) + .TopN(1) + .GetEnumerableTypedResultAsync())?.FirstOrDefault(); + + if (contactInfo is null) + { + contactInfo = new ContactInfo(); + } + + foreach (var converter in contactKenticoConverters) + { + await converter.Convert(contact, contactInfo); + } + + if (contactInfo.HasChanged) + { + contactInfoProvider.Set(contactInfo); + await syncItemService.LogContactSyncItem(contactInfo, contact.Id.ToString(), CRMType.Dynamics); + } + + } + catch (Exception e) + { + logger.LogError(e, "Syncing to contact info {ContactEmail} failed", contact.EMailAddress1); + } + } + } + private async Task UpdateLeadByEmailOrCreate(ContactInfo contactInfo, IEnumerable fieldMappings) { @@ -223,10 +310,9 @@ private async Task CreateLeadAsync(ContactInfo contactInfo, IEnumerable fieldMappings) @@ -251,10 +335,9 @@ private async Task CreateContactAsync(ContactInfo contactInfo, IEnumerable fieldMappings) { if (leadEntity is Lead lead) @@ -355,82 +436,4 @@ private async Task> GetModifiedEntitiesAsync(DateT return (await serviceClient.RetrieveMultipleAsync(query)).Entities.FirstOrDefault()?.ToEntity(); } - - public async Task SynchronizeLeadsToKenticoAsync() - { - var leads = await GetModifiedLeadsAsync(DateTime.UtcNow.AddMinutes(-1)); - foreach (var lead in leads) - { - try - { - if (string.IsNullOrWhiteSpace(lead.EMailAddress1)) - { - continue; - } - - var contactInfo = (await contactInfoProvider.Get() - .WhereEquals(nameof(ContactInfo.ContactEmail), lead.EMailAddress1) - .TopN(1) - .GetEnumerableTypedResultAsync())?.FirstOrDefault(); - - if (contactInfo is null) - { - contactInfo = new ContactInfo(); - } - - foreach (var converter in leadKenticoConverters) - { - await converter.Convert(lead, contactInfo); - } - - contactInfoProvider.Set(contactInfo); - - await syncItemService.LogContactSyncItem(contactInfo, lead.Id.ToString(), CRMType.Dynamics); - } - catch (Exception e) - { - logger.LogError(e, "Syncing to contact info {ContactEmail} failed", lead.EMailAddress1); - } - } - } - - public async Task SynchronizeContactsToKenticoAsync() - { - var contacts = await GetModifiedContactsAsync(DateTime.UtcNow.AddMinutes(-1)); - foreach (var contact in contacts) - { - try - { - if (string.IsNullOrWhiteSpace(contact.EMailAddress1)) - { - continue; - } - - var contactInfo = (await contactInfoProvider.Get() - .WhereEquals(nameof(ContactInfo.ContactEmail), contact.EMailAddress1) - .TopN(1) - .GetEnumerableTypedResultAsync())?.FirstOrDefault(); - - if (contactInfo is null) - { - contactInfo = new ContactInfo(); - } - - foreach (var converter in contactKenticoConverters) - { - await converter.Convert(contact, contactInfo); - } - - contactInfoProvider.Set(contactInfo); - - await syncItemService.LogContactSyncItem(contactInfo, contact.Id.ToString(), CRMType.Dynamics); - } - catch (Exception e) - { - logger.LogError(e, "Syncing to contact info {ContactEmail} failed", contact.EMailAddress1); - } - } - } -} - - +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/ContactsSyncQueueWorker.cs b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsSyncQueueWorker.cs similarity index 55% rename from src/Kentico.Xperience.CRM.Dynamics/Synchronization/ContactsSyncQueueWorker.cs rename to src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsSyncQueueWorker.cs index 58f338b..4ee24e8 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/ContactsSyncQueueWorker.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsSyncQueueWorker.cs @@ -4,8 +4,9 @@ namespace Kentico.Xperience.CRM.Dynamics.Synchronization; -internal class ContactsSyncQueueWorker : ContactsSyncQueueWorkerBase +internal class DynamicsSyncQueueWorker : CRMSyncQueueWorkerBase { protected override string CRMName => CRMType.Dynamics; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/FailedItemsWorker.cs b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/FailedItemsWorker.cs index ec9f070..b16cdcb 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/FailedItemsWorker.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/FailedItemsWorker.cs @@ -8,6 +8,7 @@ namespace Kentico.Xperience.CRM.Dynamics.Synchronization; /// Specific thread worker for Dynamics which try to synchronize failed items. It run each 1 minute. /// internal class FailedItemsWorker : FailedSyncItemsWorkerBase { protected override string CRMName => CRMType.Dynamics; diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactToKenticoContactConverter.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactToKenticoContactConverter.cs new file mode 100644 index 0000000..e3a881a --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactToKenticoContactConverter.cs @@ -0,0 +1,23 @@ +using CMS.ContactManagement; +using Kentico.Xperience.CRM.Common.Converters; +using Salesforce.OpenApi; + +namespace Kentico.Xperience.CRM.Salesforce.Configuration; + +public class ContactToKenticoContactConverter : ICRMTypeConverter +{ + public Task Convert(ContactSObject source, ContactInfo destination) + { + destination.ContactFirstName = source.FirstName; + destination.ContactLastName = source.LastName; + destination.ContactAddress1 = source.MailingStreet; + destination.ContactCity = source.MailingCity; + destination.ContactZIP = source.MailingPostalCode; + destination.ContactMobilePhone = source.MobilePhone; + destination.ContactBusinessPhone = source.Phone; + destination.ContactEmail = source.Email; + destination.ContactNotes = source.Description; + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/LeadToKenticoContactConverter.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/LeadToKenticoContactConverter.cs new file mode 100644 index 0000000..7f4ac98 --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/LeadToKenticoContactConverter.cs @@ -0,0 +1,24 @@ +using CMS.ContactManagement; +using Kentico.Xperience.CRM.Common.Converters; +using Salesforce.OpenApi; + +namespace Kentico.Xperience.CRM.Salesforce.Configuration; + +public class LeadToKenticoContactConverter : ICRMTypeConverter +{ + public Task Convert(LeadSObject source, ContactInfo destination) + { + destination.ContactFirstName = source.FirstName; + destination.ContactLastName = source.LastName; + destination.ContactAddress1 = source.Street; + destination.ContactCity = source.City; + destination.ContactZIP = source.PostalCode; + destination.ContactMobilePhone = source.MobilePhone; + destination.ContactBusinessPhone = source.Phone; + destination.ContactEmail = source.Email; + destination.ContactNotes = source.Description; + destination.ContactCompanyName = source.Company; + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingBuilder.cs index 3cdc4a6..e991afd 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingBuilder.cs @@ -4,7 +4,6 @@ using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Mapping.Implementations; -using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Salesforce.OpenApi; @@ -95,18 +94,46 @@ public SalesforceContactMappingBuilder AddDefaultMappingForContact() } public SalesforceContactMappingBuilder AddContactToLeadConverter() - where TConverter : class, ICRMTypeConverter + where TConverter : class, ICRMTypeConverter { serviceCollection.TryAddEnumerable(ServiceDescriptor - .Scoped, TConverter>()); + .Scoped, TConverter>()); return this; } public SalesforceContactMappingBuilder AddContactToContactConverter() - where TConverter : class, ICRMTypeConverter + where TConverter : class, ICRMTypeConverter { serviceCollection.TryAddEnumerable(ServiceDescriptor - .Scoped, TConverter>()); + .Scoped, TConverter>()); + return this; + } + + public SalesforceContactMappingBuilder AddDefaultMappingToKenticoContact() + { + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, LeadToKenticoContactConverter>()); + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, ContactToKenticoContactConverter>()); + + return this; + } + + public SalesforceContactMappingBuilder AddLeadToKenticoConverter() + where TConverter : class, ICRMTypeConverter + { + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, TConverter>()); + + return this; + } + + public SalesforceContactMappingBuilder AddContactToKenticoConverter() + where TConverter : class, ICRMTypeConverter + { + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, TConverter>()); + return this; } diff --git a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/ContactsSyncFromCRMWorker.cs b/src/Kentico.Xperience.CRM.SalesForce/Synchronization/ContactsSyncFromCRMWorker.cs new file mode 100644 index 0000000..31aad29 --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Synchronization/ContactsSyncFromCRMWorker.cs @@ -0,0 +1,12 @@ +using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Synchronization; +using Kentico.Xperience.CRM.Salesforce.Configuration; +using Kentico.Xperience.CRM.Salesforce.Synchronization; + +namespace Kentico.Xperience.CRM.Salesforce; + +public class ContactsSyncFromCRMWorker : ContactSyncFromCRMWorkerBase +{ + protected override string CRMName => CRMType.Salesforce; +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs index e55ca33..1f13da8 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs @@ -1,4 +1,5 @@ using CMS.ContactManagement; +using CMS.Helpers; using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; @@ -24,6 +25,9 @@ internal class SalesforceContactsIntegrationService : ISalesforceContactsIntegra private readonly IOptionsSnapshot settings; private readonly IEnumerable> contactLeadConverters; private readonly IEnumerable> contactContactConverters; + private readonly IEnumerable> leadKenticoConverters; + private readonly IEnumerable> contactKenticoConverters; + private readonly IContactInfoProvider contactInfoProvider; public SalesforceContactsIntegrationService( SalesforceContactMappingConfiguration contactMapping, @@ -34,7 +38,10 @@ public SalesforceContactsIntegrationService( IFailedSyncItemService failedSyncItemService, IOptionsSnapshot settings, IEnumerable> contactLeadConverters, - IEnumerable> contactContactConverters) + IEnumerable> contactContactConverters, + IEnumerable> leadKenticoConverters, + IEnumerable> contactKenticoConverters, + IContactInfoProvider contactInfoProvider) { this.contactMapping = contactMapping; this.validationService = validationService; @@ -45,6 +52,9 @@ public SalesforceContactsIntegrationService( this.settings = settings; this.contactLeadConverters = contactLeadConverters; this.contactContactConverters = contactContactConverters; + this.leadKenticoConverters = leadKenticoConverters; + this.contactKenticoConverters = contactKenticoConverters; + this.contactInfoProvider = contactInfoProvider; } public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) @@ -58,7 +68,7 @@ public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) return; } - var syncItem = await syncItemService.GetContactSyncItem(contactInfo, CRMType.Dynamics); + var syncItem = await syncItemService.GetContactSyncItem(contactInfo, CRMType.Salesforce); if (syncItem is null) { @@ -84,17 +94,17 @@ public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) catch (ApiException> e) { logger.LogError(e, "Create lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); - //failedSyncItemService.LogFailedLeadItem(contactInfo, CRMType.SalesForce); + failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Salesforce); } catch (ApiException> e) { logger.LogError(e, "Create lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); - //failedSyncItemService.LogFailedLeadItem(contactInfo, CRMType.SalesForce); + failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Salesforce); } catch (ApiException e) { logger.LogError(e, "Create lead failed - unexpected api error"); - //failedSyncItemService.LogFailedLeadItem(contactInfo, CRMType.SalesForce); + failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Salesforce); } } @@ -109,7 +119,7 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) return; } - var syncItem = await syncItemService.GetContactSyncItem(contactInfo, CRMType.Dynamics); + var syncItem = await syncItemService.GetContactSyncItem(contactInfo, CRMType.Salesforce); if (syncItem is null) { @@ -117,14 +127,14 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) } else { - var existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemEntityID, nameof(LeadSObject.Id)); - if (existingLead is null) + var existingContact = await apiService.GetContactById(syncItem.CRMSyncItemEntityID, nameof(ContactSObject.Id)); + if (existingContact is null) { await UpdateContactByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); } else if (!settings.Value.IgnoreExistingRecords) { - await UpdateContactAsync(existingLead.Id!, contactInfo, contactMapping.FieldsMapping); + await UpdateContactAsync(existingContact.Id!, contactInfo, contactMapping.FieldsMapping); } else { @@ -134,41 +144,103 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) } catch (ApiException> e) { - logger.LogError(e, "Create lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); - //failedSyncItemService.LogFailedLeadItem(contactInfo, CRMType.SalesForce); + logger.LogError(e, "Create contact failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); + failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Salesforce); } catch (ApiException> e) { - logger.LogError(e, "Create lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); - //failedSyncItemService.LogFailedLeadItem(contactInfo, CRMType.SalesForce); + logger.LogError(e, "Create contact failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); + failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Salesforce); } catch (ApiException e) { - logger.LogError(e, "Create lead failed - unexpected api error"); - //failedSyncItemService.LogFailedLeadItem(contactInfo, CRMType.SalesForce); + logger.LogError(e, "Create contact failed - unexpected api error"); + failedSyncItemService.LogFailedContactItem(contactInfo, CRMType.Salesforce); } } - public Task SynchronizeLeadsToKenticoAsync() + public async Task SynchronizeLeadsToKenticoAsync() { - throw new NotImplementedException(); - } + RequestStockHelper.Add("SuppressEvents", true); + var leads = await apiService.GetModifiedLeadsAsync(DateTime.Now.AddMinutes(-1)); + foreach (var lead in leads) + { + try + { + if (string.IsNullOrWhiteSpace(lead.Email)) + { + continue; + } - public Task SynchronizeContactsToKenticoAsync() - { - throw new NotImplementedException(); - } + var contactInfo = (await contactInfoProvider.Get() + .WhereEquals(nameof(ContactInfo.ContactEmail), lead.Email) + .TopN(1) + .GetEnumerableTypedResultAsync())?.FirstOrDefault(); - private Task> GetModifiedLeadsAsync(DateTime lastSync) - { - throw new NotImplementedException(); + if (contactInfo is null) + { + contactInfo = new ContactInfo(); + } + + foreach (var converter in leadKenticoConverters) + { + await converter.Convert(lead, contactInfo); + } + + if (contactInfo.HasChanged) + { + contactInfoProvider.Set(contactInfo); + await syncItemService.LogContactSyncItem(contactInfo, lead.Id!, CRMType.Salesforce); + } + } + catch (Exception e) + { + logger.LogError(e, "Syncing to contact info {ContactEmail} failed", lead.Email); + } + } } - private Task> GetModifiedContactsAsync(DateTime lastSync) + public async Task SynchronizeContactsToKenticoAsync() { - throw new NotImplementedException(); - } + RequestStockHelper.Add("SuppressEvents", true); + var contacts = await apiService.GetModifiedContactsAsync(DateTime.UtcNow.AddMinutes(-1)); + foreach (var contact in contacts) + { + try + { + if (string.IsNullOrWhiteSpace(contact.Email)) + { + continue; + } + + var contactInfo = (await contactInfoProvider.Get() + .WhereEquals(nameof(ContactInfo.ContactEmail), contact.Email) + .TopN(1) + .GetEnumerableTypedResultAsync())?.FirstOrDefault(); + + if (contactInfo is null) + { + contactInfo = new ContactInfo(); + } + + foreach (var converter in contactKenticoConverters) + { + await converter.Convert(contact, contactInfo); + } + if (contactInfo.HasChanged) + { + contactInfoProvider.Set(contactInfo); + await syncItemService.LogContactSyncItem(contactInfo, contact.Id!, CRMType.Salesforce); + } + } + catch (Exception e) + { + logger.LogError(e, "Syncing to contact info {ContactEmail} failed", contact.Email); + } + } + } + private async Task UpdateLeadByEmailOrCreate(ContactInfo contactInfo, IEnumerable fieldMappings) { string? existingLeadId = null; @@ -233,9 +305,8 @@ private async Task CreateLeadAsync(ContactInfo contactInfo, IEnumerable fieldMappings) @@ -264,9 +334,8 @@ private async Task CreateContactAsync(ContactInfo contactInfo, IEnumerable +internal class SalesforceSyncQueueWorker : CRMSyncQueueWorkerBase { protected override string CRMName => CRMType.Salesforce; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Salesforce/Kentico.Xperience.CRM.Salesforce.csproj b/src/Kentico.Xperience.CRM.Salesforce/Kentico.Xperience.CRM.Salesforce.csproj index 0b794a6..bf60883 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Kentico.Xperience.CRM.Salesforce.csproj +++ b/src/Kentico.Xperience.CRM.Salesforce/Kentico.Xperience.CRM.Salesforce.csproj @@ -36,6 +36,5 @@ - diff --git a/src/Kentico.Xperience.CRM.Salesforce/SalesforceIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Salesforce/SalesforceIntegrationGlobalEvents.cs index b5cf9e4..c38875d 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/SalesforceIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/SalesforceIntegrationGlobalEvents.cs @@ -1,7 +1,9 @@ using CMS; using CMS.Base; +using CMS.ContactManagement; using CMS.Core; using CMS.DataEngine; +using CMS.Helpers; using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Admin; using Kentico.Xperience.CRM.Common.Constants; @@ -42,15 +44,16 @@ protected override void OnInit(ModuleInitParameters parameters) BizFormItemEvents.Insert.After += SynchronizeBizFormLead; BizFormItemEvents.Update.After += SynchronizeBizFormLead; - - ThreadWorker.Current.EnsureRunningThread(); - + + ContactInfo.TYPEINFO.Events.Insert.After += ContactSync; + ContactInfo.TYPEINFO.Events.Update.After += ContactSync; + RequestEvents.RunEndRequestTasks.Execute += (_, _) => { FailedItemsWorker.Current.EnsureRunningThread(); }; } - + private void InitializeModule(object? sender, EventArgs e) { installer?.Install(CRMType.Salesforce); @@ -58,24 +61,16 @@ private void InitializeModule(object? sender, EventArgs e) private void SynchronizeBizFormLead(object? sender, BizFormItemEventArgs e) { - var failedSyncItemsService = Service.Resolve(); - try - { - using (var serviceScope = Service.Resolve().CreateScope()) - { - var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; - if (!settings.FormLeadsEnabled) return; - - var leadsIntegrationService = serviceScope.ServiceProvider - .GetRequiredService(); + SalesforceSyncQueueWorker.Current.Enqueue(e.Item); + } + + private void ContactSync(object? sender, ObjectEventArgs args) + { + if (args.Object is not ContactInfo contactInfo || + ValidationHelper.GetBoolean(RequestStockHelper.GetItem("SuppressEvents"), false) || + !ValidationHelper.IsEmail(contactInfo.ContactEmail)) + return; - leadsIntegrationService.SynchronizeLeadAsync(e.Item).ConfigureAwait(false).GetAwaiter().GetResult(); - } - } - catch (Exception exception) - { - logger.LogError(exception, "Error occured during updating lead"); - failedSyncItemsService.LogFailedLeadItem(e.Item, CRMType.Salesforce); - } + SalesforceSyncQueueWorker.Current.Enqueue(contactInfo); } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Salesforce/SalesforceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.Salesforce/SalesforceServiceCollectionsExtensions.cs index a4fa5d2..c8fe302 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/SalesforceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/SalesforceServiceCollectionsExtensions.cs @@ -50,20 +50,27 @@ public static IServiceCollection AddKenticoCRMSalesforce(this IServiceCollection return serviceCollection; } - public static IServiceCollection AddKenticoCRMSalesforceContactsIntegration(this IServiceCollection serviceCollection, - ContactCRMType crmType, IConfiguration configuration) - => serviceCollection.AddKenticoCRMSalesForceContactsIntegration(crmType, b => { }, configuration); - - public static IServiceCollection AddKenticoCRMSalesForceContactsIntegration(this IServiceCollection serviceCollection, + public static IServiceCollection AddKenticoCRMSalesforceContactsIntegration( + this IServiceCollection serviceCollection, + ContactCRMType crmType, + IConfiguration? configuration = null, + bool useDefaultMappingToCRM = true, + bool useDefaultMappingToKentico = true) + => serviceCollection.AddKenticoCRMSalesForceContactsIntegration(crmType, b => { }, configuration, + useDefaultMappingToCRM, useDefaultMappingToKentico); + + public static IServiceCollection AddKenticoCRMSalesForceContactsIntegration( + this IServiceCollection serviceCollection, ContactCRMType crmType, Action mappingConfig, - IConfiguration configuration, - bool useDefaultMapping = true) + IConfiguration? configuration = null, + bool useDefaultMappingToCRM = true, + bool useDefaultMappingToKentico = true) { serviceCollection.AddKenticoCrmCommonContactIntegration(); - + var mappingBuilder = new SalesforceContactMappingBuilder(serviceCollection); - if (useDefaultMapping) + if (useDefaultMappingToCRM) { mappingBuilder = crmType == ContactCRMType.Lead ? mappingBuilder.AddDefaultMappingForLead() : @@ -71,10 +78,25 @@ public static IServiceCollection AddKenticoCRMSalesForceContactsIntegration(this mappingConfig(mappingBuilder); } + if (useDefaultMappingToKentico) + { + mappingBuilder.AddDefaultMappingToKenticoContact(); + } + serviceCollection.TryAddSingleton(_ => mappingBuilder.Build()); - serviceCollection.AddOptions().Bind(configuration) - .PostConfigure(s => s.ContactType = crmType); + if (configuration is null) + { + serviceCollection.AddOptions() + .Configure(ConfigureWithCMSSettings) + .PostConfigure(s => s.ContactType = crmType); + } + else + { + serviceCollection.AddOptions().Bind(configuration) + .PostConfigure(s => s.ContactType = crmType); + } + AddSalesforceCommonIntegration(serviceCollection); serviceCollection.AddScoped(); @@ -109,7 +131,8 @@ private static void AddSalesforceCommonIntegration(IServiceCollection serviceCol serviceCollection.AddHttpClient((provider, client) => { //cannot use IOptionsSnapshot, so changes in CMS settings needs restarting app to apply immediately - var settings = provider.GetRequiredService>().CurrentValue; + var settings = provider.GetRequiredService>() + .CurrentValue; if (!settings.ApiConfig.IsValid()) throw new InvalidOperationException("Missing API settings"); @@ -121,7 +144,8 @@ private static void AddSalesforceCommonIntegration(IServiceCollection serviceCol .AddClientCredentialsTokenHandler("Salesforce.api.client"); } - private static void ConfigureWithCMSSettings(SalesforceIntegrationSettings settings, ICRMSettingsService settingsService) + private static void ConfigureWithCMSSettings(SalesforceIntegrationSettings settings, + ICRMSettingsService settingsService) { var settingsInfo = settingsService.GetSettings(CRMType.Salesforce); settings.FormLeadsEnabled = settingsInfo?.CRMIntegrationSettingsFormsEnabled ?? false; diff --git a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/FailedItemsWorker.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/FailedItemsWorker.cs index bc43de9..8b9701f 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/FailedItemsWorker.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/FailedItemsWorker.cs @@ -8,7 +8,7 @@ namespace Kentico.Xperience.CRM.Salesforce.Synchronization; /// Specific thread worker for Salesforce which try to synchronize failed items. It run each 1 minute. /// internal class FailedItemsWorker : FailedSyncItemsWorkerBase + ISalesforceContactsIntegrationService, SalesforceIntegrationSettings, SalesforceApiConfig> { protected override string CRMName => CRMType.Salesforce; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/ISalesforceApiService.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/ISalesforceApiService.cs index 15f2b3a..b52fc85 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/ISalesforceApiService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/ISalesforceApiService.cs @@ -5,7 +5,7 @@ namespace Kentico.Xperience.CRM.Salesforce.Synchronization; /// /// Http typed client for Salesforce REST API /// -public interface ISalesforceApiService +internal interface ISalesforceApiService { /// /// Creates lead entity to Salesforce Leads @@ -66,4 +66,19 @@ public interface ISalesforceApiService /// /// Task GetContactByEmail(string email); + + + /// + /// + /// + /// + /// + Task> GetModifiedLeadsAsync(DateTime lastSync); + + /// + /// + /// + /// + /// + Task> GetModifiedContactsAsync(DateTime lastSync); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs index 4a3864d..e891050 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs @@ -43,7 +43,7 @@ public async Task UpdateLeadAsync(string id, LeadSObject leadSObject) => await apiClient.LeadGET2Async(id, fields); public async Task GetLeadByEmail(string email) - => await GetEntityIdByEmail(email, "Lead"); + => await GetEntityIdByEmail(email, "Lead"); public async Task CreateContactAsync(ContactSObject contact) => await apiClient.ContactPOSTAsync(MediaTypeNames.Application.Json, contact); @@ -57,11 +57,37 @@ public async Task UpdateContactAsync(string id, ContactSObject contact) public async Task GetContactByEmail(string email) => await GetEntityIdByEmail(email, "Contact"); + public async Task> GetModifiedLeadsAsync(DateTime lastSync) + => await GetModifiedRecords("Lead", lastSync); + + public async Task> GetModifiedContactsAsync(DateTime lastSync) + => await GetModifiedRecords("Contact", lastSync); + + private async Task> GetModifiedRecords(string entityName, DateTime lastSyc) + where TModel : class + { + var apiVersion = integrationSettings.Value.ApiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); + using var request = new HttpRequestMessage(HttpMethod.Get, + $"/services/data/v{apiVersion}/query?q=SELECT+FIELDS(ALL)+FROM+{entityName}+WHERE+LastModifiedDate+>=+{lastSyc.ToUniversalTime():O}+LIMIT+200"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + var response = await httpClient.SendAsync(request); + + if (response.IsSuccessStatusCode) + { + var queryResult = await response.Content.ReadFromJsonAsync>(); + return queryResult?.Records ?? Enumerable.Empty(); + } + else + { + string responseMessage = await response.Content.ReadAsStringAsync(); + throw new ApiException("Unexpected response", (int)response.StatusCode, responseMessage, null!, null); + } + } + private async Task GetEntityIdByEmail(string email, string entityName) { var apiVersion = integrationSettings.Value.ApiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); - using var request = - new HttpRequestMessage(HttpMethod.Get, + using var request = new HttpRequestMessage(HttpMethod.Get, $"/services/data/v{apiVersion}/query?q=SELECT+Id+FROM+{entityName}+WHERE+Email='{HttpUtility.UrlEncode(email)}'+ORDER+BY+CreatedDate+DESC"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); var response = await httpClient.SendAsync(request); From a2d2b57fec364bbc9df8ea62eefb06fb41c1665c Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Sun, 28 Jan 2024 18:00:33 +0100 Subject: [PATCH 11/26] new setting property, fixes after testing --- .../DancingGoat/Controllers/TestController.cs | 18 +++++++++++++ examples/DancingGoat/Program.cs | 4 +-- .../Admin/CRMIntegrationSettingsEdit.cs | 2 ++ .../Admin/CRMIntegrationSettingsModel.cs | 13 +++++++--- .../Admin/CRMModuleInstaller.cs | 26 ++++++++++++++++--- .../CRMIntegrationSettingsInfo.generated.cs | 13 ++++++++-- .../CommonIntegrationSettings.cs | 3 +++ .../Synchronization/CRMSyncQueueWorkerBase.cs | 8 ++++++ .../ContactSyncFromCRMWorkerBase.cs | 3 +-- .../FailedSyncItemsWorkerBase.cs | 6 ++--- .../Synchronization/IFailedSyncItemService.cs | 9 ++++--- .../DynamicsServiceCollectionExtensions.cs | 1 + .../SalesforceContactsIntegrationService.cs | 14 +++++++--- .../SalesforceIntegrationGlobalEvents.cs | 2 ++ .../SalesforceServiceCollectionsExtensions.cs | 1 + 15 files changed, 99 insertions(+), 24 deletions(-) diff --git a/examples/DancingGoat/Controllers/TestController.cs b/examples/DancingGoat/Controllers/TestController.cs index 1a0165f..d22b8a1 100644 --- a/examples/DancingGoat/Controllers/TestController.cs +++ b/examples/DancingGoat/Controllers/TestController.cs @@ -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; @@ -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(jsonString); + var myObject2 = JsonSerializer.Deserialize(jsonString); + + Console.WriteLine($"CreatedDate: {myObject.CreatedDate}"); + return Ok(); + } + + public class MyObject + { + [JsonProperty("CreatedDate")] + public DateTimeOffset CreatedDate { get; set; } + } } \ No newline at end of file diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index 381e7c7..45609a5 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -93,9 +93,9 @@ // builder.Services.AddDynamicsContactsIntegration(ContactCRMType.Lead, // builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); -builder.Services.AddKenticoCRMDynamicsContactsIntegration(ContactCRMType.Contact); +//builder.Services.AddKenticoCRMDynamicsContactsIntegration(ContactCRMType.Contact); -builder.Services.AddKenticoCRMSalesforceContactsIntegration(ContactCRMType.Lead); +builder.Services.AddKenticoCRMSalesforceContactsIntegration(ContactCRMType.Contact); //CRM integration registration end var app = builder.Build(); diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs index 2bdb717..0b8bb0d 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs @@ -32,6 +32,7 @@ protected override Task ProcessFormData(CRMIntegrationSettings var info = SettingsInfo ?? new CRMIntegrationSettingsInfo(); info.CRMIntegrationSettingsFormsEnabled = model.FormsEnabled; info.CRMIntegrationSettingsContactsEnabled = model.ContactsEnabled; + info.CRMIntegrationSettingsContactsTwoWaySyncEnabled = model.ContactsTwoWaySyncEnabled; info.CRMIntegrationSettingsIgnoreExistingRecords = model.IgnoreExistingRecords; info.CRMIntegrationSettingsClientId = model.ClientId; info.CRMIntegrationSettingsClientSecret = model.ClientSecret; @@ -50,6 +51,7 @@ protected override Task ProcessFormData(CRMIntegrationSettings { FormsEnabled = SettingsInfo.CRMIntegrationSettingsFormsEnabled, ContactsEnabled = SettingsInfo.CRMIntegrationSettingsContactsEnabled, + ContactsTwoWaySyncEnabled = SettingsInfo.CRMIntegrationSettingsContactsTwoWaySyncEnabled, IgnoreExistingRecords = SettingsInfo.CRMIntegrationSettingsIgnoreExistingRecords, Url = SettingsInfo.CRMIntegrationSettingsUrl, ClientId = SettingsInfo.CRMIntegrationSettingsClientId, diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsModel.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsModel.cs index fd1106a..0fbe13d 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsModel.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsModel.cs @@ -1,4 +1,5 @@ using Kentico.Xperience.Admin.Base.FormAnnotations; +using System.Reflection.Emit; namespace Kentico.Xperience.CRM.Common.Admin; @@ -13,20 +14,24 @@ public class CRMIntegrationSettingsModel [CheckBoxComponent(Label = "Contacts enabled", Order = 2)] public bool ContactsEnabled { get; set; } - [CheckBoxComponent(Label = "Ignore existing records", Order = 3)] + [CheckBoxComponent(Label = "Contacts two-way sync enabled", Order = 3)] + [VisibleIfTrue(nameof(ContactsEnabled))] + public bool ContactsTwoWaySyncEnabled { get; set; } = true; + + [CheckBoxComponent(Label = "Ignore existing records", Order = 4)] public bool IgnoreExistingRecords { get; set; } [UrlValidationRule] - [TextInputComponent(Label = "CRM URL", Order = 4)] + [TextInputComponent(Label = "CRM URL", Order = 5)] [RequiredValidationRule] public string? Url { get; set; } [RequiredValidationRule] - [TextInputComponent(Label = "Client ID", Order = 5)] + [TextInputComponent(Label = "Client ID", Order = 6)] public string? ClientId { get; set; } [RequiredValidationRule] - [PasswordComponent(Label = "Client secret", Order = 6, RequiredLength = 0, RequireDigit = false, + [PasswordComponent(Label = "Client secret", Order = 7, RequiredLength = 0, RequireDigit = false, RequireLowercase = false, RequireUppercase = false, RequiredUniqueChars = 0, RequireNonAlphanumeric = false)] public string? ClientSecret { get; set; } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs index b0d272b..70d2b3a 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs @@ -24,7 +24,6 @@ public void Install(string crmType) { var resourceInfo = InstallModule(); InstallModuleClasses(resourceInfo); - InstallCRMIntegrationSettingsClass(resourceInfo); } private ResourceInfo InstallModule() @@ -47,6 +46,7 @@ private void InstallModuleClasses(ResourceInfo resourceInfo) { InstallSyncedItemClass(resourceInfo); InstallFailedSyncItemClass(resourceInfo); + InstallCRMIntegrationSettingsClass(resourceInfo); } private void InstallSyncedItemClass(ResourceInfo resourceInfo) @@ -223,10 +223,17 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) var settingsCRM = DataClassInfoProvider.GetDataClassInfo(CRMIntegrationSettingsInfo.OBJECT_TYPE); if (settingsCRM is not null) { - return; + // //ensure to incrementaly add new field added after previous releases + // if (new FormInfo(settingsCRM.ClassFormDefinition).FieldExists(nameof(CRMIntegrationSettingsInfo + // .CRMIntegrationSettingsContactsTwoWaySyncEnabled))) + // { + // return; + // } + } + else + { + settingsCRM = DataClassInfo.New(CRMIntegrationSettingsInfo.OBJECT_TYPE); } - - settingsCRM = DataClassInfo.New(CRMIntegrationSettingsInfo.OBJECT_TYPE); settingsCRM.ClassName = CRMIntegrationSettingsInfo.TYPEINFO.ObjectClassName; settingsCRM.ClassTableName = CRMIntegrationSettingsInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); @@ -256,6 +263,17 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) Enabled = true }; formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsContactsTwoWaySyncEnabled), + Caption = "Contacts two way sync enabled", + DefaultValue = "True", + Visible = false, + DataType = "boolean", + Enabled = true + }; + formInfo.AddFormItem(formItem); formItem = new FormFieldInfo { diff --git a/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/CRMIntegrationSettingsInfo.generated.cs b/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/CRMIntegrationSettingsInfo.generated.cs index 4714a23..48242f2 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/CRMIntegrationSettingsInfo.generated.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/CRMIntegrationSettingsInfo.generated.cs @@ -63,8 +63,17 @@ public virtual bool CRMIntegrationSettingsContactsEnabled get => ValidationHelper.GetBoolean(GetValue(nameof(CRMIntegrationSettingsContactsEnabled)), false); set => SetValue(nameof(CRMIntegrationSettingsContactsEnabled), value); } - - + + /// + /// When true, data are synced from CRM to Kentico. Relevant only when is true. + /// + [DatabaseField] + public virtual bool CRMIntegrationSettingsContactsTwoWaySyncEnabled + { + get => ValidationHelper.GetBoolean(GetValue(nameof(CRMIntegrationSettingsContactsTwoWaySyncEnabled)), true); + set => SetValue(nameof(CRMIntegrationSettingsContactsTwoWaySyncEnabled), value); + } + /// /// CRM integration settings ignore existing records. /// diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs index 16a2395..da9dc59 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs @@ -18,6 +18,9 @@ namespace Kentico.Xperience.CRM.Common.Configuration; /// public bool ContactsEnabled { get; set; } + /// + /// If enabled contacts are synced from CRM to Kentico + /// public bool ContactsTwoWaySyncEnabled { get; set; } /// diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncQueueWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncQueueWorkerBase.cs index 755e0a8..4bd957a 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncQueueWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncQueueWorkerBase.cs @@ -15,6 +15,14 @@ namespace Kentico.Xperience.CRM.Common.Workers; +/// +/// Base class for contacts synchronization from CRM to Kentico +/// +/// +/// +/// +/// +/// public abstract class CRMSyncQueueWorkerBase : ThreadQueueWorker diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs index 20b3ee3..d16a68f 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs @@ -37,8 +37,7 @@ protected override void Process() using (var scope = Service.Resolve().CreateScope()) { var settings = scope.ServiceProvider.GetRequiredService>().Value; - //@TODO new setting for 2-way - if (!settings.ContactsEnabled) return; + if (!settings.ContactsEnabled || !settings.ContactsTwoWaySyncEnabled) return; var contactsIntegrationService = scope.ServiceProvider.GetRequiredService(); diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemsWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemsWorkerBase.cs index 3183878..9752555 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemsWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemsWorkerBase.cs @@ -47,7 +47,7 @@ protected override void Process() using var serviceScope = Service.Resolve().CreateScope(); var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; - if (!settings.FormLeadsEnabled) return; + if (!settings.FormLeadsEnabled && !settings.ContactsEnabled) return; var failedSyncItemsService = Service.Resolve(); @@ -57,7 +57,7 @@ protected override void Process() foreach (var syncItem in failedSyncItemsService.GetFailedSyncItemsToReSync(CRMName)) { // contacts - if (syncItem.FailedSyncItemEntityClass == ContactInfo.TYPEINFO.ObjectClassName) + if (syncItem.FailedSyncItemEntityClass == ContactInfo.TYPEINFO.ObjectClassName && settings.ContactsEnabled) { contactsIntegrationService ??= serviceScope.ServiceProvider.GetRequiredService(); @@ -74,7 +74,7 @@ protected override void Process() .GetAwaiter().GetResult(); } //form submissions - else + else if (settings.FormLeadsEnabled) { leadsIntegrationService ??= serviceScope.ServiceProvider .GetRequiredService(); diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/IFailedSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/IFailedSyncItemService.cs index 9b030bc..7d28346 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/IFailedSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/IFailedSyncItemService.cs @@ -18,14 +18,15 @@ public interface IFailedSyncItemService void LogFailedLeadItem(BizFormItem bizFormItem, string crmName); /// - /// @TODO + /// Creates new record in failed items table or increment TrySyncCount property when record exists. + /// Next sync time is planned. /// /// /// void LogFailedContactItem(ContactInfo contactInfo, string crmName); /// - /// Creates new record in failed items table or increment TrySyncCount property when record exists. + /// Creates new records in failed items table or increment TrySyncCount property when record exists. /// Next sync time is planned. /// /// BizForm item @@ -33,8 +34,8 @@ public interface IFailedSyncItemService void LogFailedLeadItems(IEnumerable bizFormItems, string crmName); /// - /// @TODO - /// + /// Creates new records in failed items table or increment TrySyncCount property when record exists. + /// Next sync time is planned. /// /// void LogFailedContactItems(IEnumerable contactInfos, string crmName); diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index 5fd6bc5..660ee58 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -129,6 +129,7 @@ private static void ConfigureWithCMSSettings(DynamicsIntegrationSettings setting var settingsInfo = settingsService.GetSettings(CRMType.Dynamics); settings.FormLeadsEnabled = settingsInfo?.CRMIntegrationSettingsFormsEnabled ?? false; settings.ContactsEnabled = settingsInfo?.CRMIntegrationSettingsContactsEnabled ?? false; + settings.ContactsTwoWaySyncEnabled = settingsInfo?.CRMIntegrationSettingsContactsTwoWaySyncEnabled ?? true; settings.IgnoreExistingRecords = settingsInfo?.CRMIntegrationSettingsIgnoreExistingRecords ?? false; diff --git a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs index 1f13da8..3f35920 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs @@ -76,7 +76,7 @@ public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) } else { - var existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemEntityID, nameof(LeadSObject.Id)); + var existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemCRMID, nameof(LeadSObject.Id)); if (existingLead is null) { await UpdateLeadByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); @@ -127,7 +127,7 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) } else { - var existingContact = await apiService.GetContactById(syncItem.CRMSyncItemEntityID, nameof(ContactSObject.Id)); + var existingContact = await apiService.GetContactById(syncItem.CRMSyncItemCRMID, nameof(ContactSObject.Id)); if (existingContact is null) { await UpdateContactByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); @@ -300,7 +300,10 @@ private async Task CreateLeadAsync(ContactInfo contactInfo, IEnumerable lead.AdditionalProperties[m.CrmFieldName] = formFieldValue, diff --git a/src/Kentico.Xperience.CRM.Salesforce/SalesforceIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Salesforce/SalesforceIntegrationGlobalEvents.cs index c38875d..25f68fb 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/SalesforceIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/SalesforceIntegrationGlobalEvents.cs @@ -50,6 +50,8 @@ protected override void OnInit(ModuleInitParameters parameters) RequestEvents.RunEndRequestTasks.Execute += (_, _) => { + SalesforceSyncQueueWorker.Current.EnsureRunningThread(); + ContactsSyncFromCRMWorker.Current.EnsureRunningThread(); FailedItemsWorker.Current.EnsureRunningThread(); }; } diff --git a/src/Kentico.Xperience.CRM.Salesforce/SalesforceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.Salesforce/SalesforceServiceCollectionsExtensions.cs index c8fe302..f9159a6 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/SalesforceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/SalesforceServiceCollectionsExtensions.cs @@ -150,6 +150,7 @@ private static void ConfigureWithCMSSettings(SalesforceIntegrationSettings setti var settingsInfo = settingsService.GetSettings(CRMType.Salesforce); settings.FormLeadsEnabled = settingsInfo?.CRMIntegrationSettingsFormsEnabled ?? false; settings.ContactsEnabled = settingsInfo?.CRMIntegrationSettingsContactsEnabled ?? false; + settings.ContactsTwoWaySyncEnabled = settingsInfo?.CRMIntegrationSettingsContactsTwoWaySyncEnabled ?? false; settings.IgnoreExistingRecords = settingsInfo?.CRMIntegrationSettingsIgnoreExistingRecords ?? false; From ee9e19b80a90e1d71d7bff10b8fc130c25be6add Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Mon, 29 Jan 2024 18:15:16 +0100 Subject: [PATCH 12/26] datetime offset fix, new class for contacts sync time wip --- examples/DancingGoat/Program.cs | 2 +- .../Admin/CRMModuleInstaller.cs | 62 ++++++++++++++++--- .../ContactSyncFromCRMWorkerBase.cs | 11 +++- .../Configuration/DateTimeOffsetConverter.cs | 22 +++++++ .../ContactsSyncFromCRMWorker.cs | 2 +- .../Synchronization/SalesforceApiService.cs | 10 ++- 6 files changed, 97 insertions(+), 12 deletions(-) create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Configuration/DateTimeOffsetConverter.cs diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index 45609a5..a1b0c57 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -95,7 +95,7 @@ //builder.Services.AddKenticoCRMDynamicsContactsIntegration(ContactCRMType.Contact); -builder.Services.AddKenticoCRMSalesforceContactsIntegration(ContactCRMType.Contact); +builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Contact); //CRM integration registration end var app = builder.Build(); diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs index 70d2b3a..2776a91 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs @@ -47,8 +47,9 @@ private void InstallModuleClasses(ResourceInfo resourceInfo) InstallSyncedItemClass(resourceInfo); InstallFailedSyncItemClass(resourceInfo); InstallCRMIntegrationSettingsClass(resourceInfo); + InstallContactsLastSyncTimeClass(resourceInfo); } - + private void InstallSyncedItemClass(ResourceInfo resourceInfo) { var failedSyncItemClass = DataClassInfoProvider.GetDataClassInfo(CRMSyncItemInfo.OBJECT_TYPE); @@ -223,12 +224,12 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) var settingsCRM = DataClassInfoProvider.GetDataClassInfo(CRMIntegrationSettingsInfo.OBJECT_TYPE); if (settingsCRM is not null) { - // //ensure to incrementaly add new field added after previous releases - // if (new FormInfo(settingsCRM.ClassFormDefinition).FieldExists(nameof(CRMIntegrationSettingsInfo - // .CRMIntegrationSettingsContactsTwoWaySyncEnabled))) - // { - // return; - // } + //ensure to incrementaly add new field added after previous releases + if (new FormInfo(settingsCRM.ClassFormDefinition).FieldExists(nameof(CRMIntegrationSettingsInfo + .CRMIntegrationSettingsContactsTwoWaySyncEnabled))) + { + return; + } } else { @@ -337,4 +338,51 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) DataClassInfoProvider.SetDataClassInfo(settingsCRM); } + + private void InstallContactsLastSyncTimeClass(ResourceInfo resourceInfo) + { + var lastSyncTimeClass = DataClassInfoProvider.GetDataClassInfo("kenticocrmcommon.contactslastsync"); + if (lastSyncTimeClass is not null) + { + return; + } + else + { + lastSyncTimeClass = DataClassInfo.New("kenticocrmcommon.contactslastsync"); + } + + lastSyncTimeClass.ClassName = "KenticoCRMCommon.ContactsLastSync"; + lastSyncTimeClass.ClassTableName = "KenticoCRMCommon.ContactsLastSync".Replace(".", "_"); + lastSyncTimeClass.ClassDisplayName = "Contacts last sync"; + lastSyncTimeClass.ClassResourceID = resourceInfo.ResourceID; + lastSyncTimeClass.ClassType = ClassType.OTHER; + + var formInfo = + FormHelper.GetBasicFormDefinition("ContactsLastSyncItemID"); + + var formItem = new FormFieldInfo + { + Name = "ContactsLastSyncCRM", + Visible = true, + Precision = 0, + Size = 50, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = "ContactsLastSyncTime", + Visible = true, + Precision = 3, + DataType = "datetime", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + lastSyncTimeClass.ClassFormDefinition = formInfo.GetXmlDefinition(); + + DataClassInfoProvider.SetDataClassInfo(lastSyncTimeClass); + } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs index d16a68f..9ca8f84 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs @@ -41,11 +41,15 @@ protected override void Process() var contactsIntegrationService = scope.ServiceProvider.GetRequiredService(); - + + var dateBeforeSync = DateTime.Now; + (settings.ContactType == ContactCRMType.Lead ? contactsIntegrationService.SynchronizeLeadsToKenticoAsync() : contactsIntegrationService.SynchronizeContactsToKenticoAsync()) .GetAwaiter().GetResult(); + + } } catch (Exception e) @@ -53,6 +57,11 @@ protected override void Process() logger.LogError(e, "Error occured during contacts sync"); } } + + private void LogLastSuccessfulSyncTime(DateTime dateTime) + { + + } protected override void Finish() { } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/DateTimeOffsetConverter.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/DateTimeOffsetConverter.cs new file mode 100644 index 0000000..504b0a4 --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/DateTimeOffsetConverter.cs @@ -0,0 +1,22 @@ +using System.Diagnostics; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Kentico.Xperience.CRM.Salesforce.Configuration; + +/// +/// To prevent errors in parsing DateTimeOffset from SalesForce. +/// +internal class DateTimeOffsetConverter : JsonConverter +{ + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return DateTimeOffset.Parse(reader.GetString()!, CultureInfo.InvariantCulture); + } + + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } +} diff --git a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/ContactsSyncFromCRMWorker.cs b/src/Kentico.Xperience.CRM.SalesForce/Synchronization/ContactsSyncFromCRMWorker.cs index 31aad29..5ef3b92 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/ContactsSyncFromCRMWorker.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Synchronization/ContactsSyncFromCRMWorker.cs @@ -5,7 +5,7 @@ namespace Kentico.Xperience.CRM.Salesforce; -public class ContactsSyncFromCRMWorker : ContactSyncFromCRMWorkerBase { protected override string CRMName => CRMType.Salesforce; diff --git a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs index e891050..e719000 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs @@ -6,6 +6,7 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using System.Net.Mime; +using System.Text.Json; using System.Web; using SalesforceApiClient = Salesforce.OpenApi.SalesforceApiClient; @@ -17,6 +18,11 @@ internal class SalesforceApiService : ISalesforceApiService private readonly ILogger logger; private readonly IOptionsSnapshot integrationSettings; private readonly SalesforceApiClient apiClient; + + private static JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.General) + { + Converters = { new DateTimeOffsetConverter() } + }; public SalesforceApiService( HttpClient httpClient, @@ -74,7 +80,7 @@ private async Task> GetModifiedRecords(string entity if (response.IsSuccessStatusCode) { - var queryResult = await response.Content.ReadFromJsonAsync>(); + var queryResult = await response.Content.ReadFromJsonAsync>(SerializerOptions); return queryResult?.Records ?? Enumerable.Empty(); } else @@ -94,7 +100,7 @@ private async Task> GetModifiedRecords(string entity if (response.IsSuccessStatusCode) { - var queryResult = await response.Content.ReadFromJsonAsync>(); + var queryResult = await response.Content.ReadFromJsonAsync>(SerializerOptions); return queryResult?.Records.FirstOrDefault()?.Id; } else From 254bbc2b5007e3e409c126777fe189af0837d3d0 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Tue, 30 Jan 2024 17:02:29 +0100 Subject: [PATCH 13/26] contact last sync custom class, fixes --- .../Admin/CRMIntegrationSettingsEdit.cs | 2 +- .../Admin/CRMModuleInstaller.cs | 20 ++- .../ContactsLastSyncInfo.generated.cs | 116 ++++++++++++++++++ .../ContactsLastSyncInfoProvider.generated.cs | 19 +++ ...IContactsLastSyncInfoProvider.generated.cs | 11 ++ .../ContactSyncFromCRMWorkerBase.cs | 29 +++-- .../IContactsIntegrationService.cs | 4 +- .../DynamicsContactsIntegrationService.cs | 17 ++- .../SalesforceContactsIntegrationService.cs | 39 ++++-- .../SalesforceLeadsIntegrationService.cs | 10 +- 10 files changed, 223 insertions(+), 44 deletions(-) create mode 100644 src/Kentico.Xperience.CRM.Common/Admin/InfoModels/ContactsLastSyncInfo.generated.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Admin/InfoModels/ContactsLastSyncInfoProvider.generated.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Admin/InfoModels/IContactsLastSyncInfoProvider.generated.cs diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs index 0b8bb0d..29d7322 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs @@ -32,7 +32,7 @@ protected override Task ProcessFormData(CRMIntegrationSettings var info = SettingsInfo ?? new CRMIntegrationSettingsInfo(); info.CRMIntegrationSettingsFormsEnabled = model.FormsEnabled; info.CRMIntegrationSettingsContactsEnabled = model.ContactsEnabled; - info.CRMIntegrationSettingsContactsTwoWaySyncEnabled = model.ContactsTwoWaySyncEnabled; + info.CRMIntegrationSettingsContactsTwoWaySyncEnabled = model.ContactsEnabled && model.ContactsTwoWaySyncEnabled; info.CRMIntegrationSettingsIgnoreExistingRecords = model.IgnoreExistingRecords; info.CRMIntegrationSettingsClientId = model.ClientId; info.CRMIntegrationSettingsClientSecret = model.ClientSecret; diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs index 6ea2057..3234165 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs @@ -341,28 +341,26 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) private void InstallContactsLastSyncTimeClass(ResourceInfo resourceInfo) { - var lastSyncTimeClass = DataClassInfoProvider.GetDataClassInfo("kenticocrmcommon.contactslastsync"); + var lastSyncTimeClass = DataClassInfoProvider.GetDataClassInfo(ContactsLastSyncInfo.OBJECT_TYPE); if (lastSyncTimeClass is not null) { return; } - else - { - lastSyncTimeClass = DataClassInfo.New("kenticocrmcommon.contactslastsync"); - } - lastSyncTimeClass.ClassName = "KenticoCRMCommon.ContactsLastSync"; - lastSyncTimeClass.ClassTableName = "KenticoCRMCommon.ContactsLastSync".Replace(".", "_"); - lastSyncTimeClass.ClassDisplayName = "Contacts last sync"; + lastSyncTimeClass = DataClassInfo.New(ContactsLastSyncInfo.OBJECT_TYPE); + + lastSyncTimeClass.ClassName = ContactsLastSyncInfo.TYPEINFO.ObjectClassName; + lastSyncTimeClass.ClassTableName = ContactsLastSyncInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); + lastSyncTimeClass.ClassDisplayName = "CRM Contacts last sync"; lastSyncTimeClass.ClassResourceID = resourceInfo.ResourceID; lastSyncTimeClass.ClassType = ClassType.OTHER; var formInfo = - FormHelper.GetBasicFormDefinition("ContactsLastSyncItemID"); + FormHelper.GetBasicFormDefinition(nameof(ContactsLastSyncInfo.ContactsLastSyncItemID)); var formItem = new FormFieldInfo { - Name = "ContactsLastSyncCRM", + Name = nameof(ContactsLastSyncInfo.ContactsLastSyncCRM), Visible = true, Precision = 0, Size = 50, @@ -373,7 +371,7 @@ private void InstallContactsLastSyncTimeClass(ResourceInfo resourceInfo) formItem = new FormFieldInfo { - Name = "ContactsLastSyncTime", + Name = nameof(ContactsLastSyncInfo.ContactsLastSyncTime), Visible = true, Precision = 3, DataType = "datetime", diff --git a/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/ContactsLastSyncInfo.generated.cs b/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/ContactsLastSyncInfo.generated.cs new file mode 100644 index 0000000..e5ffcb4 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/ContactsLastSyncInfo.generated.cs @@ -0,0 +1,116 @@ +using System; +using System.Data; +using System.Runtime.Serialization; + +using CMS; +using CMS.DataEngine; +using CMS.Helpers; +using Kentico.Xperience.CRM.Common; + +[assembly: RegisterObjectType(typeof(ContactsLastSyncInfo), ContactsLastSyncInfo.OBJECT_TYPE)] + +namespace Kentico.Xperience.CRM.Common +{ + /// + /// Data container class for . + /// + [Serializable] + public partial class ContactsLastSyncInfo : AbstractInfo + { + /// + /// Object type. + /// + public const string OBJECT_TYPE = "kenticocrmcommon.contactslastsync"; + + + /// + /// Type information. + /// +#warning "You will need to configure the type info." + public static readonly ObjectTypeInfo TYPEINFO = new ObjectTypeInfo(typeof(ContactsLastSyncInfoProvider), OBJECT_TYPE, "KenticoCRMCommon.ContactsLastSync", "ContactsLastSyncItemID", null, null, null, null, null, null, null) + { + TouchCacheDependencies = true, + }; + + + /// + /// Contacts last sync item ID. + /// + [DatabaseField] + public virtual int ContactsLastSyncItemID + { + get => ValidationHelper.GetInteger(GetValue(nameof(ContactsLastSyncItemID)), 0); + set => SetValue(nameof(ContactsLastSyncItemID), value); + } + + + /// + /// Contacts last sync CRM. + /// + [DatabaseField] + public virtual string ContactsLastSyncCRM + { + get => ValidationHelper.GetString(GetValue(nameof(ContactsLastSyncCRM)), String.Empty); + set => SetValue(nameof(ContactsLastSyncCRM), value); + } + + + /// + /// Contacts last sync time. + /// + [DatabaseField] + public virtual DateTime ContactsLastSyncTime + { + get => ValidationHelper.GetDateTime(GetValue(nameof(ContactsLastSyncTime)), DateTimeHelper.ZERO_TIME); + set => SetValue(nameof(ContactsLastSyncTime), value); + } + + + /// + /// Deletes the object using appropriate provider. + /// + protected override void DeleteObject() + { + Provider.Delete(this); + } + + + /// + /// Updates the object using appropriate provider. + /// + protected override void SetObject() + { + Provider.Set(this); + } + + + /// + /// Constructor for de-serialization. + /// + /// Serialization info. + /// Streaming context. + protected ContactsLastSyncInfo(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + + /// + /// Creates an empty instance of the class. + /// + public ContactsLastSyncInfo() + : base(TYPEINFO) + { + } + + + /// + /// Creates a new instances of the class from the given . + /// + /// DataRow with the object data. + public ContactsLastSyncInfo(DataRow dr) + : base(TYPEINFO, dr) + { + } + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/ContactsLastSyncInfoProvider.generated.cs b/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/ContactsLastSyncInfoProvider.generated.cs new file mode 100644 index 0000000..e906d0e --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/ContactsLastSyncInfoProvider.generated.cs @@ -0,0 +1,19 @@ +using CMS.DataEngine; + +namespace Kentico.Xperience.CRM.Common +{ + /// + /// Class providing management. + /// + [ProviderInterface(typeof(IContactsLastSyncInfoProvider))] + public partial class ContactsLastSyncInfoProvider : AbstractInfoProvider, IContactsLastSyncInfoProvider + { + /// + /// Initializes a new instance of the class. + /// + public ContactsLastSyncInfoProvider() + : base(ContactsLastSyncInfo.TYPEINFO) + { + } + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/IContactsLastSyncInfoProvider.generated.cs b/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/IContactsLastSyncInfoProvider.generated.cs new file mode 100644 index 0000000..2aeb1a8 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/IContactsLastSyncInfoProvider.generated.cs @@ -0,0 +1,11 @@ +using CMS.DataEngine; + +namespace Kentico.Xperience.CRM.Common +{ + /// + /// Declares members for management. + /// + public partial interface IContactsLastSyncInfoProvider : IInfoProvider, IInfoByIdProvider + { + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs index 9ca8f84..a9c26b5 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/ContactSyncFromCRMWorkerBase.cs @@ -1,5 +1,6 @@ using CMS.Base; using CMS.Core; +using Kentic.Xperience.CRM.Common; using Kentico.Xperience.CRM.Common.Configuration; using Kentico.Xperience.CRM.Common.Enums; using Kentico.Xperience.CRM.Common.Services; @@ -19,13 +20,15 @@ public abstract class { protected override int DefaultInterval => 60000; private ILogger logger = null!; - + private IContactsLastSyncInfoProvider lastSyncInfoProvider = null!; + protected abstract string CRMName { get; } protected override void Initialize() { base.Initialize(); logger = Service.Resolve>(); + lastSyncInfoProvider = Service.Resolve(); } protected override void Process() @@ -41,15 +44,19 @@ protected override void Process() var contactsIntegrationService = scope.ServiceProvider.GetRequiredService(); - + + var lastSync = GetLastSyncInfo(); + var lastSyncTime = lastSync?.ContactsLastSyncTime ?? DateTime.Now.AddMinutes(-1); var dateBeforeSync = DateTime.Now; - (settings.ContactType == ContactCRMType.Lead ? - contactsIntegrationService.SynchronizeLeadsToKenticoAsync() : - contactsIntegrationService.SynchronizeContactsToKenticoAsync()) + (settings.ContactType == ContactCRMType.Lead + ? contactsIntegrationService.SynchronizeLeadsToKenticoAsync(lastSyncTime) + : contactsIntegrationService.SynchronizeContactsToKenticoAsync(lastSyncTime)) .GetAwaiter().GetResult(); - - + + (lastSync ??= new ContactsLastSyncInfo { ContactsLastSyncCRM = CRMName }).ContactsLastSyncTime = + dateBeforeSync; + lastSyncInfoProvider.Set(lastSync); } } catch (Exception e) @@ -58,10 +65,10 @@ protected override void Process() } } - private void LogLastSuccessfulSyncTime(DateTime dateTime) - { - - } + private ContactsLastSyncInfo? GetLastSyncInfo() => lastSyncInfoProvider.Get() + .WhereEquals(nameof(ContactsLastSyncInfo.ContactsLastSyncCRM), CRMName) + .TopN(1) + .FirstOrDefault(); protected override void Finish() { } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/IContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/IContactsIntegrationService.cs index a56b02d..d0e007f 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/IContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/IContactsIntegrationService.cs @@ -22,11 +22,11 @@ public interface IContactsIntegrationService /// Creates or updates contact info from CRM lead /// /// - Task SynchronizeLeadsToKenticoAsync(); + Task SynchronizeLeadsToKenticoAsync(DateTime lastSync); /// /// Creates or updates contact info from CRM contact /// /// - Task SynchronizeContactsToKenticoAsync(); + Task SynchronizeContactsToKenticoAsync(DateTime lastSync); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs index 40d4b02..c458145 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs @@ -165,10 +165,10 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) } - public async Task SynchronizeLeadsToKenticoAsync() + public async Task SynchronizeLeadsToKenticoAsync(DateTime lastSync) { RequestStockHelper.Add("SuppressEvents", true); - var leads = await GetModifiedLeadsAsync(DateTime.UtcNow.AddMinutes(-1)); + var leads = await GetModifiedLeadsAsync(lastSync); foreach (var lead in leads) { try @@ -193,7 +193,6 @@ public async Task SynchronizeLeadsToKenticoAsync() await converter.Convert(lead, contactInfo); } - if (contactInfo.HasChanged) { contactInfoProvider.Set(contactInfo); @@ -207,10 +206,10 @@ public async Task SynchronizeLeadsToKenticoAsync() } } - public async Task SynchronizeContactsToKenticoAsync() + public async Task SynchronizeContactsToKenticoAsync(DateTime lastSync) { RequestStockHelper.Add("SuppressEvents", true); - var contacts = await GetModifiedContactsAsync(DateTime.UtcNow.AddMinutes(-1)); + var contacts = await GetModifiedContactsAsync(lastSync); foreach (var contact in contacts) { try @@ -258,7 +257,7 @@ private async Task UpdateLeadByEmailOrCreate(ContactInfo contactInfo, if (!string.IsNullOrWhiteSpace(tmpLead.EMailAddress1)) { - existingLead = await GetLeadByEmail(tmpLead.EMailAddress1, Lead.EntityLogicalName); + existingLead = await GetEntityByEmail(tmpLead.EMailAddress1, Lead.EntityLogicalName); } if (existingLead is null) @@ -284,7 +283,7 @@ private async Task UpdateContactByEmailOrCreate(ContactInfo contactInfo, if (!string.IsNullOrWhiteSpace(tmpContact.EMailAddress1)) { - existingContact = await GetLeadByEmail(tmpContact.EMailAddress1, Contact.EntityLogicalName); + existingContact = await GetEntityByEmail(tmpContact.EMailAddress1, Contact.EntityLogicalName); } if (existingContact is null) @@ -428,10 +427,10 @@ private async Task> GetModifiedEntitiesAsync(DateT where TEntity : Entity => (await serviceClient.RetrieveAsync(logicalName, leadId, new ColumnSet(true)))?.ToEntity(); - private async Task GetLeadByEmail(string email, string logicalName) + private async Task GetEntityByEmail(string email, string logicalName) where TEntity : Entity { - var query = new QueryExpression(Lead.EntityLogicalName) { ColumnSet = new ColumnSet(true), TopCount = 1 }; + var query = new QueryExpression(logicalName) { ColumnSet = new ColumnSet(true), TopCount = 1 }; query.Criteria.AddCondition("emailaddress1", ConditionOperator.Equal, email); return (await serviceClient.RetrieveMultipleAsync(query)).Entities.FirstOrDefault()?.ToEntity(); diff --git a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs index 3f35920..1ec6057 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs @@ -76,7 +76,16 @@ public async Task SynchronizeContactToLeadsAsync(ContactInfo contactInfo) } else { - var existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemCRMID, nameof(LeadSObject.Id)); + LeadSObject? existingLead = null; + try + { + existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemCRMID, nameof(LeadSObject.Id)); + } + catch (Exception) + { + //exception means de-facto 404-NotFound status + } + if (existingLead is null) { await UpdateLeadByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); @@ -127,7 +136,16 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) } else { - var existingContact = await apiService.GetContactById(syncItem.CRMSyncItemCRMID, nameof(ContactSObject.Id)); + ContactSObject? existingContact = null; + try + { + existingContact = await apiService.GetContactById(syncItem.CRMSyncItemCRMID, nameof(ContactSObject.Id)); + } + catch (Exception) + { + //exception means de-facto 404-NotFound status + } + if (existingContact is null) { await UpdateContactByEmailOrCreate(contactInfo, contactMapping.FieldsMapping); @@ -159,10 +177,10 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) } } - public async Task SynchronizeLeadsToKenticoAsync() + public async Task SynchronizeLeadsToKenticoAsync(DateTime lastSync) { RequestStockHelper.Add("SuppressEvents", true); - var leads = await apiService.GetModifiedLeadsAsync(DateTime.Now.AddMinutes(-1)); + var leads = await apiService.GetModifiedLeadsAsync(lastSync); foreach (var lead in leads) { try @@ -200,10 +218,10 @@ public async Task SynchronizeLeadsToKenticoAsync() } } - public async Task SynchronizeContactsToKenticoAsync() + public async Task SynchronizeContactsToKenticoAsync(DateTime lastSync) { RequestStockHelper.Add("SuppressEvents", true); - var contacts = await apiService.GetModifiedContactsAsync(DateTime.UtcNow.AddMinutes(-1)); + var contacts = await apiService.GetModifiedContactsAsync(lastSync); foreach (var contact in contacts) { try @@ -240,8 +258,9 @@ public async Task SynchronizeContactsToKenticoAsync() } } } - - private async Task UpdateLeadByEmailOrCreate(ContactInfo contactInfo, IEnumerable fieldMappings) + + private async Task UpdateLeadByEmailOrCreate(ContactInfo contactInfo, + IEnumerable fieldMappings) { string? existingLeadId = null; @@ -267,7 +286,8 @@ private async Task UpdateLeadByEmailOrCreate(ContactInfo contactInfo, IEnumerabl } } - private async Task UpdateContactByEmailOrCreate(ContactInfo contactInfo, IEnumerable fieldMappings) + private async Task UpdateContactByEmailOrCreate(ContactInfo contactInfo, + IEnumerable fieldMappings) { string? existingLeadId = null; @@ -372,6 +392,7 @@ protected async Task MapLead(ContactInfo contactInfo, LeadSObject lead, //dot not try to set empty value and send them as null to prevent api errors continue; } + _ = fieldMapping.CRMFieldMapping switch { CRMFieldNameMapping m => lead.AdditionalProperties[m.CrmFieldName] = formFieldValue, diff --git a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceLeadsIntegrationService.cs index 3bca9d6..544f729 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceLeadsIntegrationService.cs @@ -91,7 +91,15 @@ private async Task SynchronizeLeadAsync(BizFormItem bizFormItem, } else { - var existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemCRMID, nameof(LeadSObject.Id)); + LeadSObject? existingLead = null; + try + { + existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemCRMID, nameof(LeadSObject.Id)); + } + catch (Exception) + { + //exception means de-facto 404-NotFound status + } if (existingLead is null) { await UpdateByEmailOrCreate(bizFormItem, fieldMappings, converters); From 469a73c05b1a8be197d7c36b3013c41241b9a961 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Fri, 2 Feb 2024 16:35:02 +0100 Subject: [PATCH 14/26] installer change for new class --- .../Admin/CRMModuleInstaller.cs | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs index 81345a1..f5792e5 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs @@ -49,7 +49,7 @@ private void InstallModuleClasses(ResourceInfo resourceInfo) InstallCRMIntegrationSettingsClass(resourceInfo); InstallContactsLastSyncTimeClass(resourceInfo); } - + private void InstallSyncedItemClass(ResourceInfo resourceInfo) { var info = DataClassInfoProvider.GetDataClassInfo(CRMSyncItemInfo.OBJECT_TYPE) ?? DataClassInfo.New(CRMSyncItemInfo.OBJECT_TYPE); @@ -245,7 +245,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) Enabled = true }; formInfo.AddFormItem(formItem); - + formItem = new FormFieldInfo { Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsContactsTwoWaySyncEnabled), @@ -340,22 +340,16 @@ private static void SetFormDefinition(DataClassInfo info, FormInfo form) info.ClassFormDefinition = form.GetXmlDefinition(); } } - + private void InstallContactsLastSyncTimeClass(ResourceInfo resourceInfo) { - var lastSyncTimeClass = DataClassInfoProvider.GetDataClassInfo(ContactsLastSyncInfo.OBJECT_TYPE); - if (lastSyncTimeClass is not null) - { - return; - } - - lastSyncTimeClass = DataClassInfo.New(ContactsLastSyncInfo.OBJECT_TYPE); + var info = DataClassInfoProvider.GetDataClassInfo(ContactsLastSyncInfo.OBJECT_TYPE) ?? DataClassInfo.New(ContactsLastSyncInfo.OBJECT_TYPE); - lastSyncTimeClass.ClassName = ContactsLastSyncInfo.TYPEINFO.ObjectClassName; - lastSyncTimeClass.ClassTableName = ContactsLastSyncInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); - lastSyncTimeClass.ClassDisplayName = "CRM Contacts last sync"; - lastSyncTimeClass.ClassResourceID = resourceInfo.ResourceID; - lastSyncTimeClass.ClassType = ClassType.OTHER; + info.ClassName = ContactsLastSyncInfo.TYPEINFO.ObjectClassName; + info.ClassTableName = ContactsLastSyncInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); + info.ClassDisplayName = "CRM Contacts last sync"; + info.ClassResourceID = resourceInfo.ResourceID; + info.ClassType = ClassType.OTHER; var formInfo = FormHelper.GetBasicFormDefinition(nameof(ContactsLastSyncInfo.ContactsLastSyncItemID)); @@ -370,7 +364,7 @@ private void InstallContactsLastSyncTimeClass(ResourceInfo resourceInfo) Enabled = true }; formInfo.AddFormItem(formItem); - + formItem = new FormFieldInfo { Name = nameof(ContactsLastSyncInfo.ContactsLastSyncTime), @@ -380,9 +374,12 @@ private void InstallContactsLastSyncTimeClass(ResourceInfo resourceInfo) Enabled = true }; formInfo.AddFormItem(formItem); - - lastSyncTimeClass.ClassFormDefinition = formInfo.GetXmlDefinition(); - DataClassInfoProvider.SetDataClassInfo(lastSyncTimeClass); + SetFormDefinition(info, formInfo); + + if (info.HasChanged) + { + DataClassInfoProvider.SetDataClassInfo(info); + } } } \ No newline at end of file From 0338d351d8b16a5a7101200c355a3ac4687294c4 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Sun, 4 Feb 2024 17:41:44 +0100 Subject: [PATCH 15/26] main readme for contacts, test samples, email fix --- README.md | 33 +++++- examples/DancingGoat/Program.cs | 2 +- .../DynamicsContactsIntegrationScenarios.cs | 111 ++++++++++++++++++ .../TestSamples/DynamicsTestConverters.cs | 86 ++++++++++++++ .../SalesforceContactsIntegrationScenarios.cs | 110 +++++++++++++++++ .../TestSamples/SalesforceTestConverters.cs | 85 ++++++++++++++ .../DynamicsServiceCollectionExtensions.cs | 2 +- .../SalesforceContactsIntegrationService.cs | 25 +++- .../SalesforceServiceCollectionsExtensions.cs | 6 +- .../SalesforceLeadsIntegrationService.cs | 13 +- 10 files changed, 455 insertions(+), 18 deletions(-) create mode 100644 examples/DancingGoat/TestSamples/DynamicsContactsIntegrationScenarios.cs create mode 100644 examples/DancingGoat/TestSamples/DynamicsTestConverters.cs create mode 100644 examples/DancingGoat/TestSamples/SalesforceContactsIntegrationScenarios.cs create mode 100644 examples/DancingGoat/TestSamples/SalesforceTestConverters.cs diff --git a/README.md b/README.md index 2343221..3fc3fcb 100644 --- a/README.md +++ b/README.md @@ -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)); @@ -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 @@ -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(DancingGoatContactUsItem.CLASS_NAME)); @@ -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)); @@ -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 @@ -154,6 +149,34 @@ Use this option when you need complex logic and need to use another service via builder.AddFormWithConverter(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. diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index a1b0c57..dad0eea 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -93,7 +93,7 @@ // builder.Services.AddDynamicsContactsIntegration(ContactCRMType.Lead, // builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); -//builder.Services.AddKenticoCRMDynamicsContactsIntegration(ContactCRMType.Contact); +builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Contact); builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Contact); //CRM integration registration end diff --git a/examples/DancingGoat/TestSamples/DynamicsContactsIntegrationScenarios.cs b/examples/DancingGoat/TestSamples/DynamicsContactsIntegrationScenarios.cs new file mode 100644 index 0000000..6f89bda --- /dev/null +++ b/examples/DancingGoat/TestSamples/DynamicsContactsIntegrationScenarios.cs @@ -0,0 +1,111 @@ +using CMS.ContactManagement; +using Kentico.Xperience.CRM.Common.Enums; +using Kentico.Xperience.CRM.Dynamics; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; + +namespace DancingGoat.TestSamples; + +internal static class DynamicsContactsIntegrationScenarios +{ + public static void InitDynamicsContactsIntegration(this WebApplicationBuilder builder) + { + //InitToLeadsSimple(builder); + InitToLeadsCustomMapping(builder); + } + + private static void InitToLeadsSimple(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Lead); + } + + private static void InitToContactsSimple(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Contact); + } + + private static void InitToLeadsCustomMapping(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Lead, builder => + builder.MapField(nameof(ContactInfo.ContactEmail), "emailaddress1") + .MapField(c => c.ContactFirstName, "firstname") + .MapField(nameof(ContactInfo.ContactLastName), e => e.LastName) + .MapField(c => c.ContactMobilePhone, e => e.MobilePhone), + useDefaultMappingToCRM: false); + } + + private static void InitToContactsCustomMapping(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Contact, builder => + builder.MapField(nameof(ContactInfo.ContactEmail), "emailaddress1") + .MapField(c => c.ContactFirstName, "firstname") + .MapField(nameof(ContactInfo.ContactLastName), e => e.LastName) + .MapField(c => c.ContactMobilePhone, e => e.MobilePhone), + useDefaultMappingToCRM: false); + } + + private static void InitToLeadsDefaultMappingAndCustomMapField(WebApplicationBuilder builder) + { + //should rewrite value for Description from default mapping + builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Lead, builder => + builder.MapField(c => $"Created in admin: {c.ContactCreatedInAdministration}", e => e.Description)); + } + + private static void InitToContactsDefaultMappingAndCustomMapField(WebApplicationBuilder builder) + { + //should rewrite value for description from default mapping + builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Contact, builder => + builder.MapField(c => $"Created in admin: {c.ContactCreatedInAdministration}", e => e.Description)); + } + + private static void InitToLeadsCustomConverter(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Lead, builder => + builder.AddContactToLeadConverter(), + useDefaultMappingToCRM: false); // when true default mapping is applied after custom converter + } + + private static void InitToContactsCustomConverter(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Lead, builder => + builder.AddContactToContactConverter(), + useDefaultMappingToCRM: false); // when true default mapping is applied after custom converter + } + + private static void InitToLeadsCustomConverterToKentico(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Lead, builder => + builder.AddLeadToKenticoConverter(), + useDefaultMappingToKentico: false); // when true then both (custom and default) converter are applied + } + + private static void InitToContactsCustomConverterToKentico(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Contact, builder => + builder.AddContactToKenticoConverter(), + useDefaultMappingToKentico: false); // when true then both (custom and default) converter are applied + } + + private static void InitToLeadsComplex(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Lead, builder => + builder.MapField(nameof(ContactInfo.ContactEmail), "emailaddress1") + .MapField(c => c.ContactFirstName, "firstname") + .MapField(nameof(ContactInfo.ContactLastName), e => e.LastName) + .MapField(c => c.ContactMobilePhone, e => e.MobilePhone) + .AddContactToLeadConverter() + .AddLeadToKenticoConverter(), + useDefaultMappingToCRM: false, useDefaultMappingToKentico: false); + } + + private static void InitToContactsComplex(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Contact, builder => + builder.MapField(nameof(ContactInfo.ContactEmail), "emailaddress1") + .MapField(c => c.ContactFirstName, "firstname") + .MapField(nameof(ContactInfo.ContactLastName), e => e.LastName) + .MapField(c => c.ContactMobilePhone, e => e.MobilePhone) + .AddContactToContactConverter() + .AddContactToKenticoConverter(), + useDefaultMappingToCRM: false, useDefaultMappingToKentico: false); + } +} diff --git a/examples/DancingGoat/TestSamples/DynamicsTestConverters.cs b/examples/DancingGoat/TestSamples/DynamicsTestConverters.cs new file mode 100644 index 0000000..dc80b9c --- /dev/null +++ b/examples/DancingGoat/TestSamples/DynamicsTestConverters.cs @@ -0,0 +1,86 @@ +using CMS.ContactManagement; +using Kentico.Xperience.CRM.Common.Converters; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; + +namespace DancingGoat.TestSamples; + +internal class DynamicsContactToLeadCustomConverter : ICRMTypeConverter +{ + public Task Convert(ContactInfo source, Lead destination) + { + //some mapping when updating + if (destination.Id != Guid.Empty) + { + destination.EMailAddress1 = source.ContactEmail; + } + //mapping for create + else + { + destination.EMailAddress1 = source.ContactEmail; + } + + return Task.CompletedTask; + } +} + +internal class DynamicsContactToContactCustomConverter : ICRMTypeConverter +{ + public Task Convert(ContactInfo source, Contact destination) + { + //some mapping when updating + if (destination.Id != Guid.Empty) + { + destination.EMailAddress1 = source.ContactEmail; + } + //mapping for create + else + { + destination.EMailAddress1 = source.ContactEmail; + } + + return Task.CompletedTask; + } +} + +internal class DynamicsLeadToKenticoContactCustomConverter : ICRMTypeConverter +{ + public Task Convert(Lead 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; + } +} + +internal class DynamicsContactToKenticoContactCustomConverter : ICRMTypeConverter +{ + 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; + } +} + diff --git a/examples/DancingGoat/TestSamples/SalesforceContactsIntegrationScenarios.cs b/examples/DancingGoat/TestSamples/SalesforceContactsIntegrationScenarios.cs new file mode 100644 index 0000000..4390da1 --- /dev/null +++ b/examples/DancingGoat/TestSamples/SalesforceContactsIntegrationScenarios.cs @@ -0,0 +1,110 @@ +using CMS.ContactManagement; +using Kentico.Xperience.CRM.Common.Enums; + +namespace DancingGoat.TestSamples; + +internal static class SalesforceContactsIntegrationScenarios +{ + + public static void InitSalesforceContactsIntegration(this WebApplicationBuilder builder) + { + //InitToLeadsSimple(builder); + InitToLeadsCustomMapping(builder); + } + + private static void InitToLeadsSimple(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Lead); + } + + private static void InitToContactsSimple(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Contact); + } + + private static void InitToLeadsCustomMapping(WebApplicationBuilder builder) + { + 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); + } + + private static void InitToContactsCustomMapping(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Contact, builder => + builder.MapField(nameof(ContactInfo.ContactEmail), "Email") + .MapField(c => c.ContactFirstName, "FirstName") + .MapContactField(nameof(ContactInfo.ContactLastName), e => e.LastName) + .MapContactField(c => c.ContactMobilePhone, e => e.MobilePhone), + useDefaultMappingToCRM: false); + } + + private static void InitToLeadsDefaultMappingAndCustomMapField(WebApplicationBuilder builder) + { + //should rewrite value for Description from default mapping + builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Lead, builder => + builder.MapLeadField(c => $"Created in admin: {c.ContactCreatedInAdministration}", e => e.Description)); + } + + private static void InitToContactsDefaultMappingAndCustomMapField(WebApplicationBuilder builder) + { + //should rewrite value for description from default mapping + builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Contact, builder => + builder.MapContactField(c => $"Created in admin: {c.ContactCreatedInAdministration}", e => e.Description)); + } + + private static void InitToLeadsCustomConverter(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Lead, builder => + builder.AddContactToLeadConverter(), + useDefaultMappingToCRM: false); // when true default mapping is applied after custom converter + } + + private static void InitToContactsCustomConverter(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Lead, builder => + builder.AddContactToContactConverter(), + useDefaultMappingToCRM: false); // when true default mapping is applied after custom converter + } + + private static void InitToLeadsCustomConverterToKentico(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Lead, builder => + builder.AddLeadToKenticoConverter(), + useDefaultMappingToKentico: false); // when true then both (custom and default) converter are applied + } + + private static void InitToContactsCustomConverterToKentico(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Contact, builder => + builder.AddContactToKenticoConverter(), + useDefaultMappingToKentico: false); // when true then both (custom and default) converter are applied + } + + private static void InitToLeadsComplex(WebApplicationBuilder builder) + { + 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) + .AddContactToLeadConverter() + .AddLeadToKenticoConverter(), + useDefaultMappingToCRM: false, useDefaultMappingToKentico: false); + } + + private static void InitToContactsComplex(WebApplicationBuilder builder) + { + builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Contact, builder => + builder.MapField(nameof(ContactInfo.ContactEmail), "Email") + .MapField(c => c.ContactFirstName, "FirstName") + .MapContactField(nameof(ContactInfo.ContactLastName), e => e.LastName) + .MapContactField(c => c.ContactMobilePhone, e => e.MobilePhone) + .AddContactToContactConverter() + .AddContactToKenticoConverter(), + useDefaultMappingToCRM: false, useDefaultMappingToKentico: false); + } +} \ No newline at end of file diff --git a/examples/DancingGoat/TestSamples/SalesforceTestConverters.cs b/examples/DancingGoat/TestSamples/SalesforceTestConverters.cs new file mode 100644 index 0000000..4f150f5 --- /dev/null +++ b/examples/DancingGoat/TestSamples/SalesforceTestConverters.cs @@ -0,0 +1,85 @@ +using CMS.ContactManagement; +using Kentico.Xperience.CRM.Common.Converters; +using Salesforce.OpenApi; + +namespace DancingGoat.TestSamples; + +internal class SalesforceContactToLeadCustomConverter : ICRMTypeConverter +{ + public Task Convert(ContactInfo source, LeadSObject destination) + { + //some mapping when updating + if (destination.Id != null) + { + destination.Email = source.ContactEmail; + } + //mapping for create + else + { + destination.Email = source.ContactEmail; + } + + return Task.CompletedTask; + } +} + +internal class SalesforceContactToContactCustomConverter : ICRMTypeConverter +{ + public Task Convert(ContactInfo source, ContactSObject destination) + { + //some mapping when updating + if (destination.Id != null) + { + destination.Email = source.ContactEmail; + } + //mapping for create + else + { + destination.Email = source.ContactEmail; + } + + return Task.CompletedTask; + } +} + +internal class SalesforceLeadToKenticoContactCustomConverter : ICRMTypeConverter +{ + 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; + } +} + +internal class SalesforceContactToKenticoContactCustomConverter : ICRMTypeConverter +{ + public Task Convert(ContactSObject 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.CleanStatus}"; + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index 660ee58..046f581 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -71,8 +71,8 @@ public static IServiceCollection AddKenticoCRMDynamicsContactsIntegration( mappingBuilder = crmType == ContactCRMType.Lead ? mappingBuilder.AddDefaultMappingForLead() : mappingBuilder.AddDefaultMappingForContact(); - mappingConfig(mappingBuilder); } + mappingConfig(mappingBuilder); if (useDefaultMappingToKentico) { diff --git a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs index 1ec6057..c6dfb40 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs @@ -139,7 +139,8 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) ContactSObject? existingContact = null; try { - existingContact = await apiService.GetContactById(syncItem.CRMSyncItemCRMID, nameof(ContactSObject.Id)); + existingContact = + await apiService.GetContactById(syncItem.CRMSyncItemCRMID, nameof(ContactSObject.Id)); } catch (Exception) { @@ -267,9 +268,16 @@ private async Task UpdateLeadByEmailOrCreate(ContactInfo contactInfo, var tmpLead = new LeadSObject(); await MapLead(contactInfo, tmpLead, fieldMappings); - if (!string.IsNullOrWhiteSpace(tmpLead.Email)) + string? emailAddress = tmpLead.Email; + if (string.IsNullOrWhiteSpace(emailAddress)) { - existingLeadId = await apiService.GetLeadByEmail(tmpLead.Email); + emailAddress = tmpLead.AdditionalProperties.TryGetValue(nameof(LeadSObject.Email), out var email) ? + email as string : + null; + } + if (!string.IsNullOrWhiteSpace(emailAddress)) + { + existingLeadId = await apiService.GetLeadByEmail(emailAddress!); } if (existingLeadId is null) @@ -294,9 +302,16 @@ private async Task UpdateContactByEmailOrCreate(ContactInfo contactInfo, var tmpLead = new ContactSObject(); await MapContact(contactInfo, tmpLead, fieldMappings); - if (!string.IsNullOrWhiteSpace(tmpLead.Email)) + string? emailAddress = tmpLead.Email; + if (string.IsNullOrWhiteSpace(emailAddress)) + { + emailAddress = tmpLead.AdditionalProperties.TryGetValue(nameof(ContactSObject.Email), out var email) ? + email as string : + null; + } + if (!string.IsNullOrWhiteSpace(emailAddress)) { - existingLeadId = await apiService.GetContactByEmail(tmpLead.Email); + existingLeadId = await apiService.GetContactByEmail(emailAddress); } if (existingLeadId is null) diff --git a/src/Kentico.Xperience.CRM.Salesforce/SalesforceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.Salesforce/SalesforceServiceCollectionsExtensions.cs index f9159a6..0bb760d 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/SalesforceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/SalesforceServiceCollectionsExtensions.cs @@ -56,10 +56,10 @@ public static IServiceCollection AddKenticoCRMSalesforceContactsIntegration( IConfiguration? configuration = null, bool useDefaultMappingToCRM = true, bool useDefaultMappingToKentico = true) - => serviceCollection.AddKenticoCRMSalesForceContactsIntegration(crmType, b => { }, configuration, + => serviceCollection.AddKenticoCRMSalesforceContactsIntegration(crmType, b => { }, configuration, useDefaultMappingToCRM, useDefaultMappingToKentico); - public static IServiceCollection AddKenticoCRMSalesForceContactsIntegration( + public static IServiceCollection AddKenticoCRMSalesforceContactsIntegration( this IServiceCollection serviceCollection, ContactCRMType crmType, Action mappingConfig, @@ -75,8 +75,8 @@ public static IServiceCollection AddKenticoCRMSalesForceContactsIntegration( mappingBuilder = crmType == ContactCRMType.Lead ? mappingBuilder.AddDefaultMappingForLead() : mappingBuilder.AddDefaultMappingForContact(); - mappingConfig(mappingBuilder); } + mappingConfig(mappingBuilder); if (useDefaultMappingToKentico) { diff --git a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceLeadsIntegrationService.cs index 544f729..1c65233 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceLeadsIntegrationService.cs @@ -140,11 +140,18 @@ private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, IEnumerable Date: Mon, 5 Feb 2024 00:03:05 +0100 Subject: [PATCH 16/26] warnings removed --- .../Admin/CRMIntegrationSettingsModel.cs | 1 - .../InfoModels/ContactsLastSyncInfo.generated.cs | 1 - .../Configuration/ContactMappingBuilder.cs | 1 - .../Converters/ICRMTypeConverter.cs | 2 +- .../Synchronization/CRMSyncQueueWorkerBase.cs | 1 - .../Synchronization/FailedSyncItemsWorkerBase.cs | 1 + .../Synchronization/IFailedSyncItemService.cs | 11 ++++++----- .../DynamicsIntegrationGlobalEvents.cs | 4 ---- .../Helpers/EntityHelper.cs | 4 ++-- .../DynamicsContactsIntegrationService.cs | 3 +-- .../Configuration/DateTimeOffsetConverter.cs | 3 +-- .../Configuration/SalesforceContactMappingBuilder.cs | 2 +- .../SalesforceContactMappingConfiguration.cs | 4 +++- .../SalesforceBizFormsMappingConfiguration.cs | 2 +- .../SalesforceIntegrationGlobalEvents.cs | 8 +------- .../Synchronization/ISalesforceApiService.cs | 10 +++++----- .../Synchronization/SalesforceApiService.cs | 4 ---- 17 files changed, 23 insertions(+), 39 deletions(-) diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsModel.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsModel.cs index 0fbe13d..ab49b1b 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsModel.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsModel.cs @@ -1,5 +1,4 @@ using Kentico.Xperience.Admin.Base.FormAnnotations; -using System.Reflection.Emit; namespace Kentico.Xperience.CRM.Common.Admin; diff --git a/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/ContactsLastSyncInfo.generated.cs b/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/ContactsLastSyncInfo.generated.cs index e5ffcb4..a68b60d 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/ContactsLastSyncInfo.generated.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/InfoModels/ContactsLastSyncInfo.generated.cs @@ -26,7 +26,6 @@ public partial class ContactsLastSyncInfo : AbstractInfo /// Type information. /// -#warning "You will need to configure the type info." public static readonly ObjectTypeInfo TYPEINFO = new ObjectTypeInfo(typeof(ContactsLastSyncInfoProvider), OBJECT_TYPE, "KenticoCRMCommon.ContactsLastSync", "ContactsLastSyncItemID", null, null, null, null, null, null, null) { TouchCacheDependencies = true, diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs index 9c3e1ee..ac25e25 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs @@ -33,7 +33,6 @@ public TBuilder MapField(Func mappingFunc, string crmFieldN /// /// Adds custom service for BizForm item validation before sending to CRM /// - /// /// /// public TBuilder AddCustomValidation() diff --git a/src/Kentico.Xperience.CRM.Common/Converters/ICRMTypeConverter.cs b/src/Kentico.Xperience.CRM.Common/Converters/ICRMTypeConverter.cs index 5bbed67..69ba8dc 100644 --- a/src/Kentico.Xperience.CRM.Common/Converters/ICRMTypeConverter.cs +++ b/src/Kentico.Xperience.CRM.Common/Converters/ICRMTypeConverter.cs @@ -1,6 +1,6 @@ namespace Kentico.Xperience.CRM.Common.Converters; -public interface ICRMTypeConverter +public interface ICRMTypeConverter { Task Convert(TSource source, TDestination destination); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncQueueWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncQueueWorkerBase.cs index 4bd957a..b80cae7 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncQueueWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/CRMSyncQueueWorkerBase.cs @@ -4,7 +4,6 @@ using CMS.DataEngine; using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Configuration; -using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Enums; using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.Common.Synchronization; diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemsWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemsWorkerBase.cs index 9752555..627713f 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemsWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/FailedSyncItemsWorkerBase.cs @@ -17,6 +17,7 @@ namespace Kentico.Xperience.CRM.Common.Synchronization; /// /// /// +/// /// /// public abstract class diff --git a/src/Kentico.Xperience.CRM.Common/Synchronization/IFailedSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Synchronization/IFailedSyncItemService.cs index 7d28346..7fd5aeb 100644 --- a/src/Kentico.Xperience.CRM.Common/Synchronization/IFailedSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Synchronization/IFailedSyncItemService.cs @@ -24,22 +24,23 @@ public interface IFailedSyncItemService /// /// void LogFailedContactItem(ContactInfo contactInfo, string crmName); - + /// /// Creates new records in failed items table or increment TrySyncCount property when record exists. /// Next sync time is planned. /// - /// BizForm item + /// /// CRM name void LogFailedLeadItems(IEnumerable bizFormItems, string crmName); /// /// Creates new records in failed items table or increment TrySyncCount property when record exists. /// Next sync time is planned. - /// + /// + /// /// void LogFailedContactItems(IEnumerable contactInfos, string crmName); - + /// /// Get all items waiting for synchronization which can be already synced again (according SyncNextTime property) /// @@ -53,7 +54,7 @@ public interface IFailedSyncItemService /// /// BizFormItem? GetBizFormItem(FailedSyncItemInfo failedSyncItemInfo); - + /// /// Delete record for given CRM, class name and ID /// diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs index 7a5dff7..c7f6f76 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -10,7 +10,6 @@ using Kentico.Xperience.CRM.Dynamics; using Kentico.Xperience.CRM.Dynamics.Synchronization; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; [assembly: RegisterModule(typeof(DynamicsIntegrationGlobalEvents))] @@ -25,7 +24,6 @@ public DynamicsIntegrationGlobalEvents() : base(nameof(DynamicsIntegrationGlobal { } - private ILogger logger = null!; private ICRMModuleInstaller? installer; protected override void OnInit(ModuleInitParameters parameters) @@ -34,7 +32,6 @@ protected override void OnInit(ModuleInitParameters parameters) var services = parameters.Services; - logger = services.GetRequiredService>(); installer = services.GetRequiredService(); ApplicationEvents.Initialized.Execute += InitializeModule; @@ -45,7 +42,6 @@ protected override void OnInit(ModuleInitParameters parameters) ContactInfo.TYPEINFO.Events.Insert.After += ContactSync; ContactInfo.TYPEINFO.Events.Update.After += ContactSync; - logger = Service.Resolve>(); Service.Resolve().Install(CRMType.Dynamics); RequestEvents.RunEndRequestTasks.Execute += (_, _) => diff --git a/src/Kentico.Xperience.CRM.Dynamics/Helpers/EntityHelper.cs b/src/Kentico.Xperience.CRM.Dynamics/Helpers/EntityHelper.cs index 3769cec..b0f3047 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Helpers/EntityHelper.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Helpers/EntityHelper.cs @@ -9,9 +9,9 @@ public static class EntityHelper /// /// Method name is returned from /// - /// /// - /// + /// + /// /// public static string GetLogicalNameFromExpression( Expression> expression) diff --git a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs index c458145..ff3e37e 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs @@ -1,5 +1,4 @@ -using CMS.Base; -using CMS.ContactManagement; +using CMS.ContactManagement; using CMS.Helpers; using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Converters; diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/DateTimeOffsetConverter.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/DateTimeOffsetConverter.cs index 504b0a4..eff50b9 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Configuration/DateTimeOffsetConverter.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/DateTimeOffsetConverter.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using System.Globalization; +using System.Globalization; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingBuilder.cs index e991afd..825bf82 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingBuilder.cs @@ -137,7 +137,7 @@ public SalesforceContactMappingBuilder AddContactToKenticoConverter( return this; } - public SalesforceContactMappingConfiguration Build() => + internal SalesforceContactMappingConfiguration Build() => new() { FieldsMapping = fieldMappings diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingConfiguration.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingConfiguration.cs index f58b3d6..4744c09 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingConfiguration.cs @@ -2,6 +2,8 @@ namespace Kentico.Xperience.CRM.Salesforce.Configuration; -public class SalesforceContactMappingConfiguration : ContactMappingConfiguration +#pragma warning disable S2094 // Classes should not be empty +internal class SalesforceContactMappingConfiguration : ContactMappingConfiguration +#pragma warning restore S2094 // Classes should not be empty { } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Salesforce/Configuration/SalesforceBizFormsMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Salesforce/Configuration/SalesforceBizFormsMappingConfiguration.cs index 77a212a..5391278 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Configuration/SalesforceBizFormsMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Configuration/SalesforceBizFormsMappingConfiguration.cs @@ -6,7 +6,7 @@ namespace Kentico.Xperience.CRM.Salesforce.Configuration; /// Specific configuration for BizForm mapping to Lead in Salesforce Sales /// #pragma warning disable S2094 // Classes should not be empty -public class SalesforceBizFormsMappingConfiguration : BizFormsMappingConfiguration +internal class SalesforceBizFormsMappingConfiguration : BizFormsMappingConfiguration #pragma warning restore S2094 // Classes should not be empty { } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Salesforce/SalesforceIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Salesforce/SalesforceIntegrationGlobalEvents.cs index 25f68fb..7bc1589 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/SalesforceIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/SalesforceIntegrationGlobalEvents.cs @@ -7,13 +7,9 @@ using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Admin; using Kentico.Xperience.CRM.Common.Constants; -using Kentico.Xperience.CRM.Common.Synchronization; using Kentico.Xperience.CRM.Salesforce; -using Kentico.Xperience.CRM.Salesforce.Configuration; using Kentico.Xperience.CRM.Salesforce.Synchronization; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; [assembly: RegisterModule(typeof(SalesforceIntegrationGlobalEvents))] @@ -24,7 +20,6 @@ namespace Kentico.Xperience.CRM.Salesforce; /// internal class SalesforceIntegrationGlobalEvents : Module { - private ILogger logger = null!; private ICRMModuleInstaller? installer; public SalesforceIntegrationGlobalEvents() : base(nameof(SalesforceIntegrationGlobalEvents)) @@ -36,8 +31,7 @@ protected override void OnInit(ModuleInitParameters parameters) base.OnInit(parameters); var services = parameters.Services; - - logger = services.GetRequiredService>(); + installer = services.GetRequiredService(); ApplicationEvents.Initialized.Execute += InitializeModule; diff --git a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/ISalesforceApiService.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/ISalesforceApiService.cs index b52fc85..bd771a0 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/ISalesforceApiService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/ISalesforceApiService.cs @@ -36,19 +36,19 @@ internal interface ISalesforceApiService /// /// Task GetLeadByEmail(string email); - + /// - /// Creates lead entity to SalesForce Leads + /// Creates contact entity to SalesForce Contacts /// - /// + /// /// Task CreateContactAsync(ContactSObject contact); /// - /// Updates lead entity to SalesForce Leads + /// Updates contact entity to SalesForce Contacts /// /// SalesForce lead ID - /// + /// /// Task UpdateContactAsync(string id, ContactSObject contact); diff --git a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs index e719000..3f98b62 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs @@ -1,5 +1,4 @@ using Kentico.Xperience.CRM.Salesforce.Configuration; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Salesforce.OpenApi; using System.Globalization; @@ -15,7 +14,6 @@ namespace Kentico.Xperience.CRM.Salesforce.Synchronization; internal class SalesforceApiService : ISalesforceApiService { private readonly HttpClient httpClient; - private readonly ILogger logger; private readonly IOptionsSnapshot integrationSettings; private readonly SalesforceApiClient apiClient; @@ -26,12 +24,10 @@ internal class SalesforceApiService : ISalesforceApiService public SalesforceApiService( HttpClient httpClient, - ILogger logger, IOptionsSnapshot integrationSettings ) { this.httpClient = httpClient; - this.logger = logger; this.integrationSettings = integrationSettings; apiClient = new SalesforceApiClient(httpClient); From 54cbfd6910785c7db4fc251c37501fd94ed57687 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Mon, 5 Feb 2024 13:28:29 +0100 Subject: [PATCH 17/26] user guide for contacts --- docs/Usage-Guide.md | 221 +++++++++++++++++- .../DynamicsContactsIntegrationScenarios.cs | 2 +- 2 files changed, 214 insertions(+), 9 deletions(-) diff --git a/docs/Usage-Guide.md b/docs/Usage-Guide.md index 202e9aa..0341970 100644 --- a/docs/Usage-Guide.md +++ b/docs/Usage-Guide.md @@ -20,14 +20,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 @@ -220,3 +221,207 @@ Use this option when you need complex logic and need to use another service via builder.Services.AddKenticoCRMSalesforce(builder => builder.AddFormWithConverter(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(nameof(ContactInfo.ContactLastName), e => e.LastName) + .MapField(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 +{ + 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(), + 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(), + 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 +{ + 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(), + 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 +{ + 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(), + 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(), + 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 SalesforceLeadToKenticoContactCustomConverter : ICRMTypeConverter +{ + 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(), + useDefaultMappingToKentico: false); // when true then both (custom and default) converter are applied +``` diff --git a/examples/DancingGoat/TestSamples/DynamicsContactsIntegrationScenarios.cs b/examples/DancingGoat/TestSamples/DynamicsContactsIntegrationScenarios.cs index 6f89bda..19b1f95 100644 --- a/examples/DancingGoat/TestSamples/DynamicsContactsIntegrationScenarios.cs +++ b/examples/DancingGoat/TestSamples/DynamicsContactsIntegrationScenarios.cs @@ -66,7 +66,7 @@ private static void InitToLeadsCustomConverter(WebApplicationBuilder builder) private static void InitToContactsCustomConverter(WebApplicationBuilder builder) { - builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Lead, builder => + builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Contact, builder => builder.AddContactToContactConverter(), useDefaultMappingToCRM: false); // when true default mapping is applied after custom converter } From 6617d7d9a546ca398f92b4674c9b5065f030c7a1 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Mon, 5 Feb 2024 14:41:46 +0100 Subject: [PATCH 18/26] fix typo in folder structure --- ...ence.CRM.Salesforce.csproj.SdkResolver.-1758752413.proj | 7 ------- .../Configuration/ContactToKenticoContactConverter.cs | 0 .../Configuration/DateTimeOffsetConverter.cs | 0 .../Configuration/LeadToKenticoContactConverter.cs | 0 .../Configuration/SalesforceContactMappingBuilder.cs | 0 .../Configuration/SalesforceContactMappingConfiguration.cs | 0 .../Synchronization/ContactsSyncFromCRMWorker.cs | 0 .../ISalesforceContactsIntegrationService.cs | 0 .../SalesforceContactsIntegrationService.cs | 0 .../Synchronization/SalesforceSyncQueueWorker.cs | 0 10 files changed, 7 deletions(-) delete mode 100644 src/Kentico.Xperience.CRM.SalesForce/Kentico.Xperience.CRM.Salesforce.csproj.SdkResolver.-1758752413.proj rename src/{Kentico.Xperience.CRM.SalesForce => Kentico.Xperience.CRM.Salesforce}/Configuration/ContactToKenticoContactConverter.cs (100%) rename src/{Kentico.Xperience.CRM.SalesForce => Kentico.Xperience.CRM.Salesforce}/Configuration/DateTimeOffsetConverter.cs (100%) rename src/{Kentico.Xperience.CRM.SalesForce => Kentico.Xperience.CRM.Salesforce}/Configuration/LeadToKenticoContactConverter.cs (100%) rename src/{Kentico.Xperience.CRM.SalesForce => Kentico.Xperience.CRM.Salesforce}/Configuration/SalesforceContactMappingBuilder.cs (100%) rename src/{Kentico.Xperience.CRM.SalesForce => Kentico.Xperience.CRM.Salesforce}/Configuration/SalesforceContactMappingConfiguration.cs (100%) rename src/{Kentico.Xperience.CRM.SalesForce => Kentico.Xperience.CRM.Salesforce}/Synchronization/ContactsSyncFromCRMWorker.cs (100%) rename src/{Kentico.Xperience.CRM.SalesForce => Kentico.Xperience.CRM.Salesforce}/Synchronization/ISalesforceContactsIntegrationService.cs (100%) rename src/{Kentico.Xperience.CRM.SalesForce => Kentico.Xperience.CRM.Salesforce}/Synchronization/SalesforceContactsIntegrationService.cs (100%) rename src/{Kentico.Xperience.CRM.SalesForce => Kentico.Xperience.CRM.Salesforce}/Synchronization/SalesforceSyncQueueWorker.cs (100%) diff --git a/src/Kentico.Xperience.CRM.SalesForce/Kentico.Xperience.CRM.Salesforce.csproj.SdkResolver.-1758752413.proj b/src/Kentico.Xperience.CRM.SalesForce/Kentico.Xperience.CRM.Salesforce.csproj.SdkResolver.-1758752413.proj deleted file mode 100644 index e643f4d..0000000 --- a/src/Kentico.Xperience.CRM.SalesForce/Kentico.Xperience.CRM.Salesforce.csproj.SdkResolver.-1758752413.proj +++ /dev/null @@ -1,7 +0,0 @@ - - - - false - D:\work\webs\xperience-by-kentico-crm\global.json - - \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactToKenticoContactConverter.cs b/src/Kentico.Xperience.CRM.Salesforce/Configuration/ContactToKenticoContactConverter.cs similarity index 100% rename from src/Kentico.Xperience.CRM.SalesForce/Configuration/ContactToKenticoContactConverter.cs rename to src/Kentico.Xperience.CRM.Salesforce/Configuration/ContactToKenticoContactConverter.cs diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/DateTimeOffsetConverter.cs b/src/Kentico.Xperience.CRM.Salesforce/Configuration/DateTimeOffsetConverter.cs similarity index 100% rename from src/Kentico.Xperience.CRM.SalesForce/Configuration/DateTimeOffsetConverter.cs rename to src/Kentico.Xperience.CRM.Salesforce/Configuration/DateTimeOffsetConverter.cs diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/LeadToKenticoContactConverter.cs b/src/Kentico.Xperience.CRM.Salesforce/Configuration/LeadToKenticoContactConverter.cs similarity index 100% rename from src/Kentico.Xperience.CRM.SalesForce/Configuration/LeadToKenticoContactConverter.cs rename to src/Kentico.Xperience.CRM.Salesforce/Configuration/LeadToKenticoContactConverter.cs diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.Salesforce/Configuration/SalesforceContactMappingBuilder.cs similarity index 100% rename from src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingBuilder.cs rename to src/Kentico.Xperience.CRM.Salesforce/Configuration/SalesforceContactMappingBuilder.cs diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Salesforce/Configuration/SalesforceContactMappingConfiguration.cs similarity index 100% rename from src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesforceContactMappingConfiguration.cs rename to src/Kentico.Xperience.CRM.Salesforce/Configuration/SalesforceContactMappingConfiguration.cs diff --git a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/ContactsSyncFromCRMWorker.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/ContactsSyncFromCRMWorker.cs similarity index 100% rename from src/Kentico.Xperience.CRM.SalesForce/Synchronization/ContactsSyncFromCRMWorker.cs rename to src/Kentico.Xperience.CRM.Salesforce/Synchronization/ContactsSyncFromCRMWorker.cs diff --git a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/ISalesforceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/ISalesforceContactsIntegrationService.cs similarity index 100% rename from src/Kentico.Xperience.CRM.SalesForce/Synchronization/ISalesforceContactsIntegrationService.cs rename to src/Kentico.Xperience.CRM.Salesforce/Synchronization/ISalesforceContactsIntegrationService.cs diff --git a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceContactsIntegrationService.cs similarity index 100% rename from src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceContactsIntegrationService.cs rename to src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceContactsIntegrationService.cs diff --git a/src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceSyncQueueWorker.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceSyncQueueWorker.cs similarity index 100% rename from src/Kentico.Xperience.CRM.SalesForce/Synchronization/SalesforceSyncQueueWorker.cs rename to src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceSyncQueueWorker.cs From 41ee5635f412a7470535df9381347dd61d52db09 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Tue, 6 Feb 2024 13:34:16 +0100 Subject: [PATCH 19/26] listing page for synced contacts --- .../Admin/CRMContactSyncItemListing.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/Kentico.Xperience.CRM.Common/Admin/CRMContactSyncItemListing.cs diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMContactSyncItemListing.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMContactSyncItemListing.cs new file mode 100644 index 0000000..5ecbe79 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMContactSyncItemListing.cs @@ -0,0 +1,35 @@ +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.Admin.DigitalMarketing.UIPages; +using Kentico.Xperience.CRM.Common.Admin; +using CMS.ContactManagement; + +[assembly: UIPage( + parentType: typeof(ContactManagementApplication), + slug: "crm-contact-sync-listing", + uiPageType: typeof(CRMContactSyncItemListing), + name: "CRM contacts sync", + templateName: TemplateNames.LISTING, + order: 1000, + icon: Icons.IntegrationScheme)] + +namespace Kentico.Xperience.CRM.Common.Admin; + +internal class CRMContactSyncItemListing : ListingPage +{ + protected override string ObjectType => CRMSyncItemInfo.OBJECT_TYPE; + + public override Task ConfigurePage() + { + PageConfiguration.ColumnConfigurations + .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityID), "Email", maxWidth: 50) + .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityCRM), "CRM", maxWidth: 20) + .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemCRMID), "CRM ID") + .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemLastModified), "Last sync"); + + PageConfiguration.QueryModifiers.AddModifier(q => + q.WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityClass), ContactInfo.TYPEINFO.ObjectClassName) + .OrderByDescending(nameof(CRMSyncItemInfo.CRMSyncItemLastModified))); + + return base.ConfigurePage(); + } +} From 379ebce39d096bf72211ab3171489f523c1ba802 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Tue, 6 Feb 2024 13:35:15 +0100 Subject: [PATCH 20/26] allow null for FailedSyncItemNextTime --- src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs index f5792e5..bfd0e3d 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs @@ -191,7 +191,8 @@ private void InstallFailedSyncItemClass(ResourceInfo resourceInfo) Visible = false, Precision = 0, DataType = "datetime", - Enabled = true + Enabled = true, + AllowEmpty = true }; formInfo.AddFormItem(formItem); From 75d08f029711d2290af2ea4e9bd43f64bdb6ba2e Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Tue, 13 Feb 2024 13:24:12 +0100 Subject: [PATCH 21/26] changes after code review --- .../Configuration/ContactMappingBuilder.cs | 4 +--- .../Configuration/ContactMappingConfiguration.cs | 2 +- .../Mapping/ContactFieldMappingFunction.cs | 2 +- .../Mapping/ContactFieldNameMapping.cs | 2 +- .../Mapping/ContactFieldToCRMMapping.cs | 2 +- .../DynamicsIntegrationGlobalEvents.cs | 2 -- .../Synchronization/DynamicsContactsIntegrationService.cs | 3 +-- .../Configuration/SalesforceContactMappingBuilder.cs | 1 - .../Synchronization/SalesforceApiService.cs | 2 +- .../Synchronization/SalesforceContactsIntegrationService.cs | 5 ++--- .../Synchronization/SalesforceLeadsIntegrationService.cs | 4 ++-- 11 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs index ac25e25..418ad21 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingBuilder.cs @@ -1,6 +1,5 @@ using CMS.ContactManagement; using Kentico.Xperience.CRM.Common.Mapping; -using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; using Microsoft.Extensions.DependencyInjection; @@ -11,8 +10,7 @@ public abstract class ContactMappingBuilder { private readonly IServiceCollection serviceCollection; protected readonly List fieldMappings = new(); - protected readonly List converters = new(); - + protected ContactMappingBuilder(IServiceCollection serviceCollection) { this.serviceCollection = serviceCollection; diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs index e40ba5d..51e0973 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/ContactMappingConfiguration.cs @@ -1,4 +1,4 @@ -using Kentico.Xperience.CRM.Common.Mapping.Implementations; +using Kentico.Xperience.CRM.Common.Mapping; namespace Kentico.Xperience.CRM.Common.Configuration; diff --git a/src/Kentico.Xperience.CRM.Common/Mapping/ContactFieldMappingFunction.cs b/src/Kentico.Xperience.CRM.Common/Mapping/ContactFieldMappingFunction.cs index 5b96e51..89db8fb 100644 --- a/src/Kentico.Xperience.CRM.Common/Mapping/ContactFieldMappingFunction.cs +++ b/src/Kentico.Xperience.CRM.Common/Mapping/ContactFieldMappingFunction.cs @@ -1,6 +1,6 @@ using CMS.ContactManagement; -namespace Kentico.Xperience.CRM.Common.Mapping.Implementations; +namespace Kentico.Xperience.CRM.Common.Mapping; public class ContactFieldMappingFunction : IContactFieldMapping { diff --git a/src/Kentico.Xperience.CRM.Common/Mapping/ContactFieldNameMapping.cs b/src/Kentico.Xperience.CRM.Common/Mapping/ContactFieldNameMapping.cs index e8c5421..82c6745 100644 --- a/src/Kentico.Xperience.CRM.Common/Mapping/ContactFieldNameMapping.cs +++ b/src/Kentico.Xperience.CRM.Common/Mapping/ContactFieldNameMapping.cs @@ -1,6 +1,6 @@ using CMS.ContactManagement; -namespace Kentico.Xperience.CRM.Common.Mapping.Implementations; +namespace Kentico.Xperience.CRM.Common.Mapping; /// /// Contact Info item field mapping based on form field name diff --git a/src/Kentico.Xperience.CRM.Common/Mapping/ContactFieldToCRMMapping.cs b/src/Kentico.Xperience.CRM.Common/Mapping/ContactFieldToCRMMapping.cs index 2ac086c..3a0e7ed 100644 --- a/src/Kentico.Xperience.CRM.Common/Mapping/ContactFieldToCRMMapping.cs +++ b/src/Kentico.Xperience.CRM.Common/Mapping/ContactFieldToCRMMapping.cs @@ -1,4 +1,4 @@ -namespace Kentico.Xperience.CRM.Common.Mapping.Implementations; +namespace Kentico.Xperience.CRM.Common.Mapping; /// /// Mapping wrapper for BizForm field mapping and Crm entity field mapping diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs index c7f6f76..3e04deb 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -42,8 +42,6 @@ protected override void OnInit(ModuleInitParameters parameters) ContactInfo.TYPEINFO.Events.Insert.After += ContactSync; ContactInfo.TYPEINFO.Events.Update.After += ContactSync; - Service.Resolve().Install(CRMType.Dynamics); - RequestEvents.RunEndRequestTasks.Execute += (_, _) => { DynamicsSyncQueueWorker.Current.EnsureRunningThread(); diff --git a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs index ff3e37e..0025040 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs @@ -3,7 +3,6 @@ using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; -using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.Common.Synchronization; using Kentico.Xperience.CRM.Dynamics.Configuration; @@ -17,7 +16,7 @@ namespace Kentico.Xperience.CRM.Dynamics.Synchronization; -public class DynamicsContactsIntegrationService : IDynamicsContactsIntegrationService +internal class DynamicsContactsIntegrationService : IDynamicsContactsIntegrationService { private readonly DynamicsContactMappingConfiguration contactMapping; private readonly IContactsIntegrationValidationService validationService; diff --git a/src/Kentico.Xperience.CRM.Salesforce/Configuration/SalesforceContactMappingBuilder.cs b/src/Kentico.Xperience.CRM.Salesforce/Configuration/SalesforceContactMappingBuilder.cs index 825bf82..7a1b409 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Configuration/SalesforceContactMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Configuration/SalesforceContactMappingBuilder.cs @@ -3,7 +3,6 @@ using Kentico.Xperience.CRM.Common.Configuration; using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; -using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Salesforce.OpenApi; diff --git a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs index 3f98b62..06f8964 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceApiService.cs @@ -17,7 +17,7 @@ internal class SalesforceApiService : ISalesforceApiService private readonly IOptionsSnapshot integrationSettings; private readonly SalesforceApiClient apiClient; - private static JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.General) + private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.General) { Converters = { new DateTimeOffsetConverter() } }; diff --git a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceContactsIntegrationService.cs index c6dfb40..5077799 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceContactsIntegrationService.cs @@ -3,7 +3,6 @@ using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Converters; using Kentico.Xperience.CRM.Common.Mapping; -using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.Common.Synchronization; using Kentico.Xperience.CRM.Salesforce.Configuration; @@ -142,9 +141,9 @@ public async Task SynchronizeContactToContactsAsync(ContactInfo contactInfo) existingContact = await apiService.GetContactById(syncItem.CRMSyncItemCRMID, nameof(ContactSObject.Id)); } - catch (Exception) + catch (ApiException e) when (e.StatusCode == 404) { - //exception means de-facto 404-NotFound status + //supress exception on 404-NotFound status } if (existingContact is null) diff --git a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceLeadsIntegrationService.cs index 1c65233..f7b64cd 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceLeadsIntegrationService.cs @@ -96,9 +96,9 @@ private async Task SynchronizeLeadAsync(BizFormItem bizFormItem, { existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemCRMID, nameof(LeadSObject.Id)); } - catch (Exception) + catch (ApiException e) when (e.StatusCode == 404) { - //exception means de-facto 404-NotFound status + //suppress exception on 404-NotFound status } if (existingLead is null) { From d97012f4ad79caf1f84a82538f8c0774907c7e0a Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Thu, 15 Feb 2024 13:58:26 +0100 Subject: [PATCH 22/26] removed warnigns and comment in program.cs --- examples/DancingGoat/Program.cs | 7 +++++++ .../Synchronization/DynamicsContactsIntegrationService.cs | 2 +- .../SalesforceContactsIntegrationService.cs | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index dad0eea..ceaf538 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -58,6 +58,7 @@ //CRM integration registration start +// manual mapping example: //builder.Services.AddKenticoCRMDynamics(builder => // builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name // c => c @@ -70,11 +71,13 @@ // , // builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //config section with settings +// Dynamics form submissions to Leads based on auto-mapping (mapping to contact fields is used from XbyK) builder.Services.AddKenticoCRMDynamics(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b .MapField(c => c.UserMessage, e => e.Description)) .AddCustomValidation()); //optional +// manual mapping example: //builder.Services.AddKenticoCRMSalesforce(builder => // builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name // c => c @@ -84,17 +87,21 @@ // .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //option 4: source mapping function general BizFormItem -> member expression to SObject // )); +// Salesforce form submissions to Leads based on auto-mapping (mapping to contact fields is used from XbyK) builder.Services.AddKenticoCRMSalesforce(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b .MapField(c => c.UserMessage, e => e.Description)) .AddCustomValidation()); +// example with settings in configuration: // builder.Services.AddDynamicsContactsIntegration(ContactCRMType.Lead, // builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); +// Dynamics contacts integration to Contacts builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMType.Contact); +// Salesforce contacts integration to Contacts builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMType.Contact); //CRM integration registration end diff --git a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs index 0025040..b202965 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Synchronization/DynamicsContactsIntegrationService.cs @@ -377,7 +377,7 @@ private async Task MapCRMEntity(ContactInfo contactInfo, Entity leadEntity, } else { - throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), + throw new ArgumentOutOfRangeException(nameof(fieldMappings), fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping"); } } diff --git a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceContactsIntegrationService.cs b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceContactsIntegrationService.cs index 5077799..a2f0e52 100644 --- a/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceContactsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Salesforce/Synchronization/SalesforceContactsIntegrationService.cs @@ -411,7 +411,7 @@ protected async Task MapLead(ContactInfo contactInfo, LeadSObject lead, { CRMFieldNameMapping m => lead.AdditionalProperties[m.CrmFieldName] = formFieldValue, CRMFieldMappingFunction m => m.MapCrmField(lead, formFieldValue), - _ => throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), + _ => throw new ArgumentOutOfRangeException(nameof(fieldMappings), fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping") }; } @@ -432,7 +432,7 @@ protected async Task MapContact(ContactInfo contactInfo, ContactSObject contact, { CRMFieldNameMapping m => contact.AdditionalProperties[m.CrmFieldName] = formFieldValue, CRMFieldMappingFunction m => m.MapCrmField(contact, formFieldValue), - _ => throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), + _ => throw new ArgumentOutOfRangeException(nameof(fieldMappings), fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping") }; } From efa5653ca067d0772c87b391704edf16cee975af Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Wed, 21 Feb 2024 16:13:41 +0100 Subject: [PATCH 23/26] custom classes as System to show them in CMS, contacts sync table screenshot, user guide updated --- docs/Usage-Guide.md | 25 +++++++++++++++--- .../screenshots/CRM_contacts_sync_table.png | Bin 0 -> 317600 bytes .../Admin/CRMModuleInstaller.cs | 8 +++--- 3 files changed, 25 insertions(+), 8 deletions(-) create mode 100644 images/screenshots/CRM_contacts_sync_table.png diff --git a/docs/Usage-Guide.md b/docs/Usage-Guide.md index 0341970..7a76887 100644 --- a/docs/Usage-Guide.md +++ b/docs/Usage-Guide.md @@ -3,6 +3,7 @@ ## 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 leads") ![Dynamics settings](../images/screenshots/Dynamics_CRM_settings.png "Dynamics CRM settings") ## CRM settings @@ -227,7 +228,7 @@ Use this option when you need complex logic and need to use another service via 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 +### Dynamics Sales Basic example how to init (default mapping from ContactInfo to CRM entity is used): @@ -288,7 +289,7 @@ Then initialize integration with custom converter: useDefaultMappingToCRM: false); // when true default mapping is applied after custom converter ``` -##### Sync from CRM to Kentico +#### 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). @@ -327,7 +328,7 @@ builder.Services.AddKenticoCRMDynamicsContactsIntegration(crmType: ContactCRMTyp useDefaultMappingToKentico: false); // when true then both (custom and default) converter are applied ``` -#### Salesforce +### Salesforce Basic example how to init (default mapping from ContactInfo to CRM entity is used): ```csharp @@ -387,7 +388,13 @@ Then initialize integration with custom converter: useDefaultMappingToCRM: false); // when true default mapping is applied after custom converter ``` -##### Sync from CRM to Kentico +#### 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). @@ -425,3 +432,13 @@ builder.Services.AddKenticoCRMSalesforceContactsIntegration(crmType: ContactCRMT builder.AddLeadToKenticoConverter(), 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. + + diff --git a/images/screenshots/CRM_contacts_sync_table.png b/images/screenshots/CRM_contacts_sync_table.png new file mode 100644 index 0000000000000000000000000000000000000000..41007dfeaaa5a6839319d88d3daeeb7f89d09517 GIT binary patch literal 317600 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%0A)I=qPFfRx7W$GPbkmN? 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�a<*a^dx1 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 z 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 literal 0 HcmV?d00001 diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs index bfd0e3d..8ee3f54 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMModuleInstaller.cs @@ -58,7 +58,7 @@ private void InstallSyncedItemClass(ResourceInfo resourceInfo) info.ClassTableName = CRMSyncItemInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); info.ClassDisplayName = "CRM Successful sync item"; info.ClassResourceID = resourceInfo.ResourceID; - info.ClassType = ClassType.OTHER; + info.ClassType = ClassType.SYSTEM_TABLE; var formInfo = FormHelper.GetBasicFormDefinition(nameof(CRMSyncItemInfo.CRMSyncItemID)); @@ -141,7 +141,7 @@ private void InstallFailedSyncItemClass(ResourceInfo resourceInfo) info.ClassTableName = FailedSyncItemInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); info.ClassDisplayName = "CRM Failed sync item"; info.ClassResourceID = resourceInfo.ResourceID; - info.ClassType = ClassType.OTHER; + info.ClassType = ClassType.SYSTEM_TABLE; var formInfo = FormHelper.GetBasicFormDefinition(nameof(FailedSyncItemInfo.FailedSyncItemID)); @@ -222,7 +222,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) info.ClassTableName = CRMIntegrationSettingsInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); info.ClassDisplayName = "CRM integration settings"; info.ClassResourceID = resourceInfo.ResourceID; - info.ClassType = ClassType.OTHER; + info.ClassType = ClassType.SYSTEM_TABLE; var formInfo = FormHelper.GetBasicFormDefinition(nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsItemID)); @@ -350,7 +350,7 @@ private void InstallContactsLastSyncTimeClass(ResourceInfo resourceInfo) info.ClassTableName = ContactsLastSyncInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); info.ClassDisplayName = "CRM Contacts last sync"; info.ClassResourceID = resourceInfo.ResourceID; - info.ClassType = ClassType.OTHER; + info.ClassType = ClassType.SYSTEM_TABLE; var formInfo = FormHelper.GetBasicFormDefinition(nameof(ContactsLastSyncInfo.ContactsLastSyncItemID)); From 7f6653c768a08d4112c4d19f6ddf93f43a600f81 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Wed, 21 Feb 2024 16:14:03 +0100 Subject: [PATCH 24/26] typo fix --- docs/Usage-Guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Usage-Guide.md b/docs/Usage-Guide.md index 7a76887..7c378f8 100644 --- a/docs/Usage-Guide.md +++ b/docs/Usage-Guide.md @@ -3,7 +3,7 @@ ## 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 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 From e8309ef98c2ed876a225a3eaf7c6f28d68bd373a Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Wed, 21 Feb 2024 16:20:32 +0100 Subject: [PATCH 25/26] user guide ToC --- docs/Usage-Guide.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/Usage-Guide.md b/docs/Usage-Guide.md index 7c378f8..409d008 100644 --- a/docs/Usage-Guide.md +++ b/docs/Usage-Guide.md @@ -1,5 +1,12 @@ # 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") From 2371b55de3dd9b90196e487b0bb6377a5c2fd9c4 Mon Sep 17 00:00:00 2001 From: Ondrej Henek Date: Thu, 14 Mar 2024 13:25:36 +0100 Subject: [PATCH 26/26] Version bump 2.0.0-reprelease --- Directory.Build.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 235d7f8..77038e6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,8 +5,8 @@ $(Company) Copyright © $(Company) $([System.DateTime]::Now.Year) $(Company)™ - 1.0.1 - + 2.0.0 + prerelease1 MIT https://github.com/Kentico/xperience-by-kentico-crm