Skip to content

Commit 685918d

Browse files
authored
Merge pull request #18 from umbraco/feature/17-update-existing-contact
Updates HubSpot contact properties if record already exists based on email address.
2 parents c5dca45 + a7a2d8f commit 685918d

File tree

5 files changed

+119
-19
lines changed

5 files changed

+119
-19
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using Newtonsoft.Json;
2+
using System.Collections.Generic;
3+
4+
namespace Umbraco.Forms.Integrations.Crm.Hubspot
5+
{
6+
internal class PropertiesRequestV1
7+
{
8+
[JsonProperty(PropertyName = "properties")]
9+
public IList<PropertyValue> Properties { get; set; } = new List<PropertyValue>();
10+
11+
internal class PropertyValue
12+
{
13+
public PropertyValue(string property, string value)
14+
{
15+
Property = property;
16+
Value = value;
17+
}
18+
19+
[JsonProperty(PropertyName = "property")]
20+
public string Property { get; }
21+
22+
[JsonProperty(PropertyName = "value")]
23+
public string Value { get; }
24+
}
25+
}
26+
}

src/Umbraco.Forms.Integrations.Crm.Hubspot/Models/Requests/PropertiesRequest.cs renamed to src/Umbraco.Forms.Integrations.Crm.Hubspot/Models/Requests/PropertiesRequestV3.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
namespace Umbraco.Forms.Integrations.Crm.Hubspot
55
{
6-
internal class PropertiesRequest
6+
internal class PropertiesRequestV3
77
{
88
[JsonProperty(PropertyName = "properties")]
99
public JObject Properties { get; set; } = new JObject();

src/Umbraco.Forms.Integrations.Crm.Hubspot/Services/HubspotContactService.cs

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ public class HubspotContactService : IContactService
3232
private readonly AppCaches _appCaches;
3333
private readonly IKeyValueService _keyValueService;
3434

35-
private const string CrmApiBaseUrl = "https://api.hubapi.com/crm/v3/";
35+
private const string CrmApiHost = "https://api.hubapi.com";
36+
private static readonly string CrmV3ApiBaseUrl = $"{CrmApiHost}/crm/v3/";
3637
private const string InstallUrlFormat = "https://app-eu1.hubspot.com/oauth/authorize?client_id={0}&redirect_uri={1}&scope={2}";
3738
private const string OAuthScopes = "oauth%20forms%20crm.objects.contacts.read%20crm.objects.contacts.write";
3839
private const string OAuthClientId = "1a04f5bf-e99e-48e1-9d62-6c25bf2bdefe";
40+
private const string JsonContentType = "application/json";
3941

4042
private const string OAuthBaseUrl = "https://hubspot-forms-auth.umbraco.com/"; // For local testing: "https://localhost:44364/"
4143
private static string OAuthRedirectUrl = OAuthBaseUrl;
@@ -102,7 +104,7 @@ public async Task<IEnumerable<Property>> GetContactPropertiesAsync()
102104
return Enumerable.Empty<Property>();
103105
}
104106

105-
var requestUrl = $"{CrmApiBaseUrl}properties/contacts";
107+
var requestUrl = $"{CrmV3ApiBaseUrl}properties/contacts";
106108
var httpMethod = HttpMethod.Get;
107109
var response = await GetResponse(requestUrl, httpMethod, authenticationDetails).ConfigureAwait(false);
108110
if (response.IsSuccessStatusCode == false)
@@ -127,7 +129,10 @@ public async Task<IEnumerable<Property>> GetContactPropertiesAsync()
127129
return properties.OrderBy(x => x.Label);
128130
}
129131

130-
public async Task<CommandResult> PostContactAsync(Record record, List<MappedProperty> fieldMappings, Dictionary<string, string> additionalFields = null)
132+
public async Task<CommandResult> PostContactAsync(Record record, List<MappedProperty> fieldMappings)
133+
=> await PostContactAsync(record, fieldMappings, null);
134+
135+
public async Task<CommandResult> PostContactAsync(Record record, List<MappedProperty> fieldMappings, Dictionary<string, string> additionalFields)
131136
{
132137
var authenticationDetails = GetConfiguredAuthenticationDetails();
133138
if (authenticationDetails.Mode == AuthenticationMode.Unauthenticated)
@@ -138,15 +143,27 @@ public async Task<CommandResult> PostContactAsync(Record record, List<MappedProp
138143

139144
// Map data from the workflow setting Hubspot fields
140145
// From the form field values submitted for this form submission
141-
var postData = new PropertiesRequest();
146+
var propertiesRequestV1 = new PropertiesRequestV1();
147+
var propertiesRequestV3 = new PropertiesRequestV3();
148+
var emailValue = string.Empty;
142149
foreach (var mapping in fieldMappings)
143150
{
144151
var fieldId = mapping.FormField;
145152
var recordField = record.GetRecordField(Guid.Parse(fieldId));
146153
if (recordField != null)
147154
{
155+
var value = recordField.ValuesAsString(false);
156+
157+
propertiesRequestV1.Properties.Add(new PropertiesRequestV1.PropertyValue(mapping.HubspotField, value));
158+
propertiesRequestV3.Properties.Add(mapping.HubspotField, value);
159+
148160
// TODO: What about different field types in forms & Hubspot that are not simple text ones?
149-
postData.Properties.Add(mapping.HubspotField, recordField.ValuesAsString(false));
161+
162+
// "Email" appears to be a special form field used for uniqueness checks, so we can safely look it up by name.
163+
if (mapping.HubspotField.ToLowerInvariant() == "email")
164+
{
165+
emailValue = value;
166+
}
150167
}
151168
else
152169
{
@@ -160,32 +177,81 @@ public async Task<CommandResult> PostContactAsync(Record record, List<MappedProp
160177
// Add any extra fields that got passed (from a custom workflow)
161178
foreach (var additionalField in additionalFields)
162179
{
163-
postData.Properties.Add(additionalField.Key, additionalField.Value);
180+
propertiesRequestV1.Properties.Add(new PropertiesRequestV1.PropertyValue(additionalField.Key, additionalField.Value));
181+
propertiesRequestV3.Properties.Add(additionalField.Key, additionalField.Value);
164182
}
165183
}
166184

167185
// POST data to hubspot
168186
// https://api.hubapi.com/crm/v3/objects/contacts?hapikey=YOUR_HUBSPOT_API_KEY
169-
var requestUrl = $"{CrmApiBaseUrl}objects/contacts";
187+
var requestUrl = $"{CrmV3ApiBaseUrl}objects/contacts";
170188
var httpMethod = HttpMethod.Post;
171-
var response = await GetResponse(requestUrl, httpMethod, authenticationDetails, postData, "application/json").ConfigureAwait(false);
189+
var response = await GetResponse(requestUrl, httpMethod, authenticationDetails, propertiesRequestV3, JsonContentType).ConfigureAwait(false);
172190

173191
// Depending on POST status fail or mark workflow as completed
174192
if (response.IsSuccessStatusCode == false)
175193
{
176-
var retryResult = await HandleFailedRequest(response.StatusCode, requestUrl, httpMethod, authenticationDetails);
177-
if (retryResult.Success)
194+
// A 409 - Conflict response indicates that the contact (by email address) already exists.
195+
if (response.StatusCode == HttpStatusCode.Conflict)
178196
{
179-
return CommandResult.Success;
197+
return await UpdateContactAsync(record, authenticationDetails, propertiesRequestV1, emailValue);
180198
}
181199
else
182200
{
183-
_logger.Error<HubspotContactService>("Error submitting a HubSpot contact request ");
184-
return CommandResult.Failed;
201+
var retryResult = await HandleFailedRequest(response.StatusCode, requestUrl, httpMethod, authenticationDetails, propertiesRequestV3, JsonContentType);
202+
if (retryResult.Success)
203+
{
204+
_logger.Info<HubspotContactService>($"Hubspot contact record created from record {record.UniqueId}.");
205+
return CommandResult.Success;
206+
}
207+
else
208+
{
209+
_logger.Error<HubspotContactService>("Error creating a HubSpot contact.");
210+
return CommandResult.Failed;
211+
}
185212
}
186213
}
214+
else
215+
{
216+
_logger.Info<HubspotContactService>($"Hubspot contact record created from record {record.UniqueId}.");
217+
return CommandResult.Success;
218+
}
219+
}
187220

188-
return CommandResult.Success;
221+
private async Task<CommandResult> UpdateContactAsync(Record record, AuthenticationDetail authenticationDetails, PropertiesRequestV1 postData, string email)
222+
{
223+
if (!string.IsNullOrEmpty(email))
224+
{
225+
// When the contact exists we can update the details using https://legacydocs.hubspot.com/docs/methods/contacts/update_contact-by-email
226+
// It uses the V1 API but support suggests it will be added to V3 before being depreciated so we can use safely:
227+
// https://community.hubspot.com/t5/APIs-Integrations/Get-Contacts-from-contact-list-using-email/m-p/419493/highlight/true#M41567
228+
var requestUrl = $"{CrmApiHost}/contacts/v1/contact/email/{email}/profile";
229+
var response = await GetResponse(requestUrl, HttpMethod.Post, authenticationDetails, postData, JsonContentType).ConfigureAwait(false);
230+
if (response.IsSuccessStatusCode == false)
231+
{
232+
var retryResult = await HandleFailedRequest(response.StatusCode, requestUrl, HttpMethod.Post, authenticationDetails, postData, JsonContentType);
233+
if (retryResult.Success)
234+
{
235+
_logger.Info<HubspotContactService>($"Hubspot contact record updated from record {record.UniqueId}.");
236+
return CommandResult.Success;
237+
}
238+
else
239+
{
240+
_logger.Error<HubspotContactService>("Error updating a HubSpot contact.");
241+
return CommandResult.Failed;
242+
}
243+
}
244+
else
245+
{
246+
_logger.Info<HubspotContactService>($"Hubspot contact record updated from record {record.UniqueId}.");
247+
return CommandResult.Success;
248+
}
249+
}
250+
else
251+
{
252+
_logger.Warn<HubspotContactService>("Could not add a new HubSpot contact due to 409/Conflict response, but no email field was provided to carry out an update.");
253+
return CommandResult.Failed;
254+
}
189255
}
190256

191257
private AuthenticationDetail GetConfiguredAuthenticationDetails()
@@ -304,7 +370,7 @@ private static HttpContent CreateRequestContent(object data, string contentType)
304370

305371
switch (contentType)
306372
{
307-
case "application/json":
373+
case JsonContentType:
308374
var serializedData = JsonConvert.SerializeObject(data);
309375
return new StringContent(serializedData, Encoding.UTF8, contentType);
310376
case "application/x-www-form-urlencoded":
@@ -314,7 +380,13 @@ private static HttpContent CreateRequestContent(object data, string contentType)
314380
}
315381
}
316382

317-
private async Task<HandleFailedRequestResult> HandleFailedRequest(HttpStatusCode statusCode, string requestUrl, HttpMethod httpMethod, AuthenticationDetail authenticationDetails)
383+
private async Task<HandleFailedRequestResult> HandleFailedRequest(
384+
HttpStatusCode statusCode,
385+
string requestUrl,
386+
HttpMethod httpMethod,
387+
AuthenticationDetail authenticationDetails,
388+
object content = null,
389+
string contentType = "")
318390
{
319391
var result = new HandleFailedRequestResult();
320392
if (authenticationDetails.Mode == AuthenticationMode.OAuth)
@@ -327,7 +399,7 @@ private async Task<HandleFailedRequestResult> HandleFailedRequest(HttpStatusCode
327399
await RefreshOAuthAccessToken(authenticationDetails.RefreshToken);
328400

329401
// Repeat the operation using the refreshed token.
330-
var response = await GetResponse(requestUrl, httpMethod, authenticationDetails).ConfigureAwait(false);
402+
var response = await GetResponse(requestUrl, httpMethod, authenticationDetails, content, contentType).ConfigureAwait(false);
331403
if (response.IsSuccessStatusCode)
332404
{
333405
result.Success = true;

src/Umbraco.Forms.Integrations.Crm.Hubspot/Services/IContactService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ public interface IContactService
2626

2727
Task<IEnumerable<Property>> GetContactPropertiesAsync();
2828

29+
Task<CommandResult> PostContactAsync(Record record, List<MappedProperty> fieldMappings);
30+
2931
Task<CommandResult> PostContactAsync(Record record, List<MappedProperty> fieldMappings, Dictionary<string, string> additionalFields);
3032
}
3133
}

src/Umbraco.Forms.Integrations.Crm.Hubspot/Umbraco.Forms.Integrations.Crm.Hubspot.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<PackageIconUrl></PackageIconUrl>
1212
<PackageProjectUrl>https://github.com/umbraco/Umbraco.Forms.Integrations</PackageProjectUrl>
1313
<RepositoryUrl>https://github.com/umbraco/Umbraco.Forms.Integrations</RepositoryUrl>
14-
<Version>1.1.2</Version>
14+
<Version>2.0.0</Version>
1515
<Authors>Umbraco HQ</Authors>
1616
<Company>Umbraco</Company>
1717
</PropertyGroup>

0 commit comments

Comments
 (0)