@@ -32,10 +32,12 @@ public class HubspotContactService : IContactService
32
32
private readonly AppCaches _appCaches ;
33
33
private readonly IKeyValueService _keyValueService ;
34
34
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/";
36
37
private const string InstallUrlFormat = "https://app-eu1.hubspot.com/oauth/authorize?client_id={0}&redirect_uri={1}&scope={2}" ;
37
38
private const string OAuthScopes = "oauth%20forms%20crm.objects.contacts.read%20crm.objects.contacts.write" ;
38
39
private const string OAuthClientId = "1a04f5bf-e99e-48e1-9d62-6c25bf2bdefe" ;
40
+ private const string JsonContentType = "application/json" ;
39
41
40
42
private const string OAuthBaseUrl = "https://hubspot-forms-auth.umbraco.com/" ; // For local testing: "https://localhost:44364/"
41
43
private static string OAuthRedirectUrl = OAuthBaseUrl ;
@@ -102,7 +104,7 @@ public async Task<IEnumerable<Property>> GetContactPropertiesAsync()
102
104
return Enumerable . Empty < Property > ( ) ;
103
105
}
104
106
105
- var requestUrl = $ "{ CrmApiBaseUrl } properties/contacts";
107
+ var requestUrl = $ "{ CrmV3ApiBaseUrl } properties/contacts";
106
108
var httpMethod = HttpMethod . Get ;
107
109
var response = await GetResponse ( requestUrl , httpMethod , authenticationDetails ) . ConfigureAwait ( false ) ;
108
110
if ( response . IsSuccessStatusCode == false )
@@ -127,7 +129,10 @@ public async Task<IEnumerable<Property>> GetContactPropertiesAsync()
127
129
return properties . OrderBy ( x => x . Label ) ;
128
130
}
129
131
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 )
131
136
{
132
137
var authenticationDetails = GetConfiguredAuthenticationDetails ( ) ;
133
138
if ( authenticationDetails . Mode == AuthenticationMode . Unauthenticated )
@@ -138,15 +143,27 @@ public async Task<CommandResult> PostContactAsync(Record record, List<MappedProp
138
143
139
144
// Map data from the workflow setting Hubspot fields
140
145
// 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 ;
142
149
foreach ( var mapping in fieldMappings )
143
150
{
144
151
var fieldId = mapping . FormField ;
145
152
var recordField = record . GetRecordField ( Guid . Parse ( fieldId ) ) ;
146
153
if ( recordField != null )
147
154
{
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
+
148
160
// 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
+ }
150
167
}
151
168
else
152
169
{
@@ -160,32 +177,81 @@ public async Task<CommandResult> PostContactAsync(Record record, List<MappedProp
160
177
// Add any extra fields that got passed (from a custom workflow)
161
178
foreach ( var additionalField in additionalFields )
162
179
{
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 ) ;
164
182
}
165
183
}
166
184
167
185
// POST data to hubspot
168
186
// 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";
170
188
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 ) ;
172
190
173
191
// Depending on POST status fail or mark workflow as completed
174
192
if ( response . IsSuccessStatusCode == false )
175
193
{
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 )
178
196
{
179
- return CommandResult . Success ;
197
+ return await UpdateContactAsync ( record , authenticationDetails , propertiesRequestV1 , emailValue ) ;
180
198
}
181
199
else
182
200
{
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
+ }
185
212
}
186
213
}
214
+ else
215
+ {
216
+ _logger . Info < HubspotContactService > ( $ "Hubspot contact record created from record { record . UniqueId } .") ;
217
+ return CommandResult . Success ;
218
+ }
219
+ }
187
220
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
+ }
189
255
}
190
256
191
257
private AuthenticationDetail GetConfiguredAuthenticationDetails ( )
@@ -304,7 +370,7 @@ private static HttpContent CreateRequestContent(object data, string contentType)
304
370
305
371
switch ( contentType )
306
372
{
307
- case "application/json" :
373
+ case JsonContentType :
308
374
var serializedData = JsonConvert . SerializeObject ( data ) ;
309
375
return new StringContent ( serializedData , Encoding . UTF8 , contentType ) ;
310
376
case "application/x-www-form-urlencoded" :
@@ -314,7 +380,13 @@ private static HttpContent CreateRequestContent(object data, string contentType)
314
380
}
315
381
}
316
382
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 = "" )
318
390
{
319
391
var result = new HandleFailedRequestResult ( ) ;
320
392
if ( authenticationDetails . Mode == AuthenticationMode . OAuth )
@@ -327,7 +399,7 @@ private async Task<HandleFailedRequestResult> HandleFailedRequest(HttpStatusCode
327
399
await RefreshOAuthAccessToken ( authenticationDetails . RefreshToken ) ;
328
400
329
401
// 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 ) ;
331
403
if ( response . IsSuccessStatusCode )
332
404
{
333
405
result . Success = true ;
0 commit comments