From 5a38222e0db834b8c5c791ff1d24ede1fdd1e054 Mon Sep 17 00:00:00 2001 From: Arthur Ceccotti Date: Wed, 8 Jul 2026 14:32:31 +0100 Subject: [PATCH 1/2] Add customer support platform identifiers to StartConversationRequest Lets callers link the customer being created to their record(s) in third-party support platforms (Intercom, Zendesk, Salesforce, Freshchat, Freshdesk), alongside customerId. Also pins jackson-core/jackson-annotations to the same version as jackson-databind: Spring Boot's imported BOM was silently downgrading them, which broke Jackson serialization needed to test this change. Co-Authored-By: Claude Sonnet 5 --- .../CustomerSupportPlatformIdentifier.java | 87 ++++++++++++++++++ ...CustomerSupportPlatformIdentifierType.java | 55 ++++++++++++ .../client/model/SupportPlatform.java | 49 ++++++++++ .../request/StartConversationRequest.java | 46 ++++++++++ .../request/StartConversationRequestTest.java | 89 +++++++++++++++++++ pom.xml | 10 +++ 6 files changed, 336 insertions(+) create mode 100644 gradient-labs-client/src/main/java/ai/gradientlabs/client/model/CustomerSupportPlatformIdentifier.java create mode 100644 gradient-labs-client/src/main/java/ai/gradientlabs/client/model/CustomerSupportPlatformIdentifierType.java create mode 100644 gradient-labs-client/src/main/java/ai/gradientlabs/client/model/SupportPlatform.java create mode 100644 gradient-labs-client/src/test/java/ai/gradientlabs/client/request/StartConversationRequestTest.java diff --git a/gradient-labs-client/src/main/java/ai/gradientlabs/client/model/CustomerSupportPlatformIdentifier.java b/gradient-labs-client/src/main/java/ai/gradientlabs/client/model/CustomerSupportPlatformIdentifier.java new file mode 100644 index 0000000..90e30e4 --- /dev/null +++ b/gradient-labs-client/src/main/java/ai/gradientlabs/client/model/CustomerSupportPlatformIdentifier.java @@ -0,0 +1,87 @@ +package ai.gradientlabs.client.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Links a customer to their record in a third-party customer support platform. + *

+ * {@code type} is only meaningful (and only validated) for platforms that have more than one + * kind of identifier: intercom, zendesk, and salesforce. Omit it entirely for freshchat and + * freshdesk, which don't have subtypes. + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CustomerSupportPlatformIdentifier { + + @JsonProperty("support_platform") + private SupportPlatform supportPlatform; + + @JsonProperty("type") + private CustomerSupportPlatformIdentifierType type; + + @JsonProperty("value") + private String value; + + /** + * Default constructor for Jackson. + */ + public CustomerSupportPlatformIdentifier() { + } + + /** + * Creates a new identifier for a platform that doesn't have subtypes (e.g. freshchat, freshdesk). + * + * @param supportPlatform the support platform + * @param value the external ID in that platform + */ + public CustomerSupportPlatformIdentifier(SupportPlatform supportPlatform, String value) { + this.supportPlatform = supportPlatform; + this.value = value; + } + + /** + * Creates a new identifier with a subtype (required for intercom, zendesk, and salesforce). + * + * @param supportPlatform the support platform + * @param type the kind of identifier + * @param value the external ID in that platform + */ + public CustomerSupportPlatformIdentifier(SupportPlatform supportPlatform, CustomerSupportPlatformIdentifierType type, String value) { + this.supportPlatform = supportPlatform; + this.type = type; + this.value = value; + } + + public SupportPlatform getSupportPlatform() { + return supportPlatform; + } + + public void setSupportPlatform(SupportPlatform supportPlatform) { + this.supportPlatform = supportPlatform; + } + + public CustomerSupportPlatformIdentifierType getType() { + return type; + } + + public void setType(CustomerSupportPlatformIdentifierType type) { + this.type = type; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public String toString() { + return "CustomerSupportPlatformIdentifier{" + + "supportPlatform=" + supportPlatform + + ", type=" + type + + ", value='" + value + '\'' + + '}'; + } +} diff --git a/gradient-labs-client/src/main/java/ai/gradientlabs/client/model/CustomerSupportPlatformIdentifierType.java b/gradient-labs-client/src/main/java/ai/gradientlabs/client/model/CustomerSupportPlatformIdentifierType.java new file mode 100644 index 0000000..436fa3d --- /dev/null +++ b/gradient-labs-client/src/main/java/ai/gradientlabs/client/model/CustomerSupportPlatformIdentifierType.java @@ -0,0 +1,55 @@ +package ai.gradientlabs.client.model; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * The kind of identifier a {@link CustomerSupportPlatformIdentifier} represents, for + * platforms that have more than one kind of customer identifier. + */ +public enum CustomerSupportPlatformIdentifierType { + /** + * An Intercom lead ID. + */ + INTERCOM_LEAD("intercom_lead"), + + /** + * An Intercom user ID. + */ + INTERCOM_USER("intercom_user"), + + /** + * A Zendesk end-user ID from a messaging conversation. + */ + ZENDESK_CONVERSATION_USER("zendesk_conversation_user"), + + /** + * A Zendesk support (ticketing) user ID. + */ + ZENDESK_SUPPORT_USER("zendesk_support_user"), + + /** + * A Salesforce Contact ID. + */ + SALESFORCE_CONTACT_ID("salesforce_contact_id"), + + /** + * A Salesforce Account ID. + */ + SALESFORCE_ACCOUNT_ID("salesforce_account_id"); + + private final String value; + + CustomerSupportPlatformIdentifierType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/gradient-labs-client/src/main/java/ai/gradientlabs/client/model/SupportPlatform.java b/gradient-labs-client/src/main/java/ai/gradientlabs/client/model/SupportPlatform.java new file mode 100644 index 0000000..a704c54 --- /dev/null +++ b/gradient-labs-client/src/main/java/ai/gradientlabs/client/model/SupportPlatform.java @@ -0,0 +1,49 @@ +package ai.gradientlabs.client.model; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * A third-party customer support platform. + */ +public enum SupportPlatform { + /** + * Intercom. + */ + INTERCOM("intercom"), + + /** + * Zendesk. + */ + ZENDESK("zendesk"), + + /** + * Salesforce. + */ + SALESFORCE("salesforce"), + + /** + * Freshchat. + */ + FRESHCHAT("freshchat"), + + /** + * Freshdesk. + */ + FRESHDESK("freshdesk"); + + private final String value; + + SupportPlatform(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartConversationRequest.java b/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartConversationRequest.java index ebe5e1d..60f6351 100644 --- a/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartConversationRequest.java +++ b/gradient-labs-client/src/main/java/ai/gradientlabs/client/request/StartConversationRequest.java @@ -1,12 +1,15 @@ package ai.gradientlabs.client.request; import ai.gradientlabs.client.model.Channel; +import ai.gradientlabs.client.model.CustomerSupportPlatformIdentifier; import ai.gradientlabs.client.model.ParticipantType; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.time.Instant; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -45,6 +48,9 @@ public class StartConversationRequest { @JsonProperty("traffic_group_id") private final String trafficGroupId; + @JsonProperty("customer_support_platform_identifiers") + private final List customerSupportPlatformIdentifiers; + private StartConversationRequest(Builder builder) { this.id = builder.id; this.customerId = builder.customerId; @@ -56,6 +62,7 @@ private StartConversationRequest(Builder builder) { this.resources = builder.resources; this.conversationToken = builder.conversationToken; this.trafficGroupId = builder.trafficGroupId; + this.customerSupportPlatformIdentifiers = builder.customerSupportPlatformIdentifiers; } public static Builder builder() { @@ -107,6 +114,15 @@ public String getTrafficGroupId() { return trafficGroupId; } + /** + * Gets the customer's identifiers in third-party customer support platforms. + * + * @return the customer support platform identifiers, or null if none were set + */ + public List getCustomerSupportPlatformIdentifiers() { + return customerSupportPlatformIdentifiers; + } + public static class Builder { private String id; private String customerId; @@ -118,6 +134,7 @@ public static class Builder { private Map resources; private String conversationToken; private String trafficGroupId; + private List customerSupportPlatformIdentifiers; private Builder() { } @@ -285,6 +302,35 @@ public Builder trafficGroupId(String trafficGroupId) { return this; } + /** + * Sets the customer's identifiers in third-party customer support platforms (optional). + *

+ * Links the customer being created to their record(s) in platforms such as Intercom + * or Zendesk, alongside {@code customerId}. Each identifier's {@code type} is only + * required (and only validated) for intercom, zendesk, and salesforce. + * + * @param customerSupportPlatformIdentifiers the customer support platform identifiers + * @return this builder + */ + public Builder customerSupportPlatformIdentifiers(List customerSupportPlatformIdentifiers) { + this.customerSupportPlatformIdentifiers = customerSupportPlatformIdentifiers; + return this; + } + + /** + * Adds a single customer support platform identifier. + * + * @param customerSupportPlatformIdentifier the identifier to add + * @return this builder + */ + public Builder addCustomerSupportPlatformIdentifier(CustomerSupportPlatformIdentifier customerSupportPlatformIdentifier) { + if (this.customerSupportPlatformIdentifiers == null) { + this.customerSupportPlatformIdentifiers = new ArrayList<>(); + } + this.customerSupportPlatformIdentifiers.add(customerSupportPlatformIdentifier); + return this; + } + /** * Builds the request. * diff --git a/gradient-labs-client/src/test/java/ai/gradientlabs/client/request/StartConversationRequestTest.java b/gradient-labs-client/src/test/java/ai/gradientlabs/client/request/StartConversationRequestTest.java new file mode 100644 index 0000000..32f382a --- /dev/null +++ b/gradient-labs-client/src/test/java/ai/gradientlabs/client/request/StartConversationRequestTest.java @@ -0,0 +1,89 @@ +package ai.gradientlabs.client.request; + +import ai.gradientlabs.client.model.Channel; +import ai.gradientlabs.client.model.CustomerSupportPlatformIdentifier; +import ai.gradientlabs.client.model.CustomerSupportPlatformIdentifierType; +import ai.gradientlabs.client.model.SupportPlatform; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class StartConversationRequestTest { + + private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new JavaTimeModule()); + + @Test + void omitsCustomerSupportPlatformIdentifiersWhenNotSet() throws Exception { + StartConversationRequest request = StartConversationRequest.builder() + .id("conv-1") + .customerId("user-1") + .channel(Channel.CHAT) + .build(); + + JsonNode json = objectMapper.valueToTree(request); + + assertFalse(json.has("customer_support_platform_identifiers")); + } + + @Test + void serializesCustomerSupportPlatformIdentifiersWithAndWithoutType() throws Exception { + StartConversationRequest request = StartConversationRequest.builder() + .id("conv-1") + .customerId("user-1") + .channel(Channel.CHAT) + .addCustomerSupportPlatformIdentifier(new CustomerSupportPlatformIdentifier( + SupportPlatform.INTERCOM, + CustomerSupportPlatformIdentifierType.INTERCOM_USER, + "6953e162a988d9ef0f73ef9b")) + .addCustomerSupportPlatformIdentifier(new CustomerSupportPlatformIdentifier( + SupportPlatform.FRESHDESK, + "12345")) + .build(); + + JsonNode json = objectMapper.valueToTree(request); + JsonNode identifiers = json.get("customer_support_platform_identifiers"); + + assertEquals(2, identifiers.size()); + + JsonNode intercomIdentifier = identifiers.get(0); + assertEquals("intercom", intercomIdentifier.get("support_platform").asText()); + assertEquals("intercom_user", intercomIdentifier.get("type").asText()); + assertEquals("6953e162a988d9ef0f73ef9b", intercomIdentifier.get("value").asText()); + + JsonNode freshdeskIdentifier = identifiers.get(1); + assertEquals("freshdesk", freshdeskIdentifier.get("support_platform").asText()); + assertEquals("12345", freshdeskIdentifier.get("value").asText()); + assertFalse(freshdeskIdentifier.has("type")); + } + + @Test + void roundTripsThroughDeserialization() throws Exception { + StartConversationRequest request = StartConversationRequest.builder() + .id("conv-1") + .customerId("user-1") + .channel(Channel.CHAT) + .addCustomerSupportPlatformIdentifier(new CustomerSupportPlatformIdentifier( + SupportPlatform.ZENDESK, + CustomerSupportPlatformIdentifierType.ZENDESK_SUPPORT_USER, + "42")) + .build(); + + String json = objectMapper.writeValueAsString(request); + JsonNode tree = objectMapper.readTree(json); + List identifiers = objectMapper.convertValue( + tree.get("customer_support_platform_identifiers"), + objectMapper.getTypeFactory().constructCollectionType(List.class, CustomerSupportPlatformIdentifier.class)); + + assertEquals(1, identifiers.size()); + assertEquals(SupportPlatform.ZENDESK, identifiers.get(0).getSupportPlatform()); + assertEquals(CustomerSupportPlatformIdentifierType.ZENDESK_SUPPORT_USER, identifiers.get(0).getType()); + assertEquals("42", identifiers.get(0).getValue()); + assertEquals(1, request.getCustomerSupportPlatformIdentifiers().size()); + } +} diff --git a/pom.xml b/pom.xml index 582174e..01a1747 100644 --- a/pom.xml +++ b/pom.xml @@ -75,6 +75,16 @@ jackson-databind ${jackson.version} + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + com.fasterxml.jackson.datatype jackson-datatype-jsr310 From bd09e3b6fe216cc8c3d2fe9ead3eb568260efd5e Mon Sep 17 00:00:00 2001 From: Arthur Ceccotti Date: Wed, 8 Jul 2026 14:58:20 +0100 Subject: [PATCH 2/2] Use jackson-bom instead of pinning individual Jackson artifacts Manually pinning jackson-databind/jackson-datatype-jsr310 but not jackson-core/jackson-annotations is how those two ended up silently downgraded by Spring Boot's BOM in the first place. Importing the official Jackson BOM (before Spring Boot's, so it wins the version conflict) manages all Jackson artifacts consistently from one place. Co-Authored-By: Claude Sonnet 5 --- pom.xml | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/pom.xml b/pom.xml index 01a1747..0744945 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,16 @@ ${project.version} + + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + + org.springframework.boot @@ -69,28 +79,6 @@ import - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - ${jackson.version} - - jakarta.servlet