From b1005af3c23076852272369eb71adeb0ea23703b Mon Sep 17 00:00:00 2001 From: vsydor Date: Thu, 28 Jul 2022 01:16:05 +0300 Subject: [PATCH] Saving sms conversations into crm task; Added temp method in crm controller for manual testing Twilio Conversation webhook added (onMessageAdded) Code review comments pulled logic into MessagingService, convering the controller endpoint into a generic conversations webhook Removed unsuned task config property; Setting task id in update method Added MBT inbound/message-status webhooks; Saving inbound messages into crm activity (tasks) Added basic processing of Mailchimp message webhooks Code review comments Using combinations of webhook data fields as a converstion key for MBT and Mailchimp minor fixes moved the logic to a new ActivityService --- .../java/com/impactupgrade/nucleus/App.java | 3 + .../nucleus/client/SfdcClient.java | 7 + .../nucleus/controller/MBTController.java | 131 ++++++++++++++++++ .../controller/MailchimpController.java | 119 +++++++++++++++- .../nucleus/controller/TwilioController.java | 47 +++++++ .../controller/TwilioFrontlineController.java | 2 +- .../nucleus/environment/Environment.java | 2 + .../environment/EnvironmentConfig.java | 2 + .../model/{CrmTask.java => CrmActivity.java} | 19 ++- .../service/logic/ActivityService.java | 51 +++++++ .../service/logic/MessagingService.java | 1 + .../service/logic/NotificationService.java | 6 +- .../service/segment/BasicCrmService.java | 12 +- .../service/segment/BloomerangCrmService.java | 14 +- .../nucleus/service/segment/CrmService.java | 6 +- .../service/segment/HubSpotCrmService.java | 30 ++-- .../service/segment/NoOpCrmService.java | 14 +- .../service/segment/SfdcCrmService.java | 83 +++++++++-- .../service/segment/SharePointCrmService.java | 14 +- .../service/segment/VirtuousCrmService.java | 36 +++-- src/main/resources/environment-sample.json | 2 + 21 files changed, 548 insertions(+), 53 deletions(-) create mode 100644 src/main/java/com/impactupgrade/nucleus/controller/MBTController.java rename src/main/java/com/impactupgrade/nucleus/model/{CrmTask.java => CrmActivity.java} (63%) create mode 100644 src/main/java/com/impactupgrade/nucleus/service/logic/ActivityService.java diff --git a/src/main/java/com/impactupgrade/nucleus/App.java b/src/main/java/com/impactupgrade/nucleus/App.java index 508ed9644..521b1cb55 100644 --- a/src/main/java/com/impactupgrade/nucleus/App.java +++ b/src/main/java/com/impactupgrade/nucleus/App.java @@ -12,6 +12,7 @@ import com.impactupgrade.nucleus.controller.EmailController; import com.impactupgrade.nucleus.controller.EventsController; import com.impactupgrade.nucleus.controller.JobController; +import com.impactupgrade.nucleus.controller.MBTController; import com.impactupgrade.nucleus.controller.MailchimpController; import com.impactupgrade.nucleus.controller.PaymentGatewayController; import com.impactupgrade.nucleus.controller.ScheduledJobController; @@ -100,6 +101,7 @@ public void start() throws Exception { apiConfig.register(twilioFrontlineController()); apiConfig.register(accountingController()); apiConfig.register(jobController()); + apiConfig.register(mbtController()); // Controllers that require DB connectivity -- prevent JDBC/Hikari connection errors. if ("true".equalsIgnoreCase(System.getenv("DATABASE_CONNECTED"))) { @@ -160,6 +162,7 @@ public void registerServlets(ServletContextHandler context) throws Exception {} protected ScheduledJobController scheduledJobController() { return new ScheduledJobController(envFactory); } protected AccountingController accountingController() { return new AccountingController(envFactory); } protected JobController jobController() { return new JobController(envFactory); } + protected MBTController mbtController() { return new MBTController(envFactory); } public EnvironmentFactory getEnvironmentFactory() { return envFactory; } diff --git a/src/main/java/com/impactupgrade/nucleus/client/SfdcClient.java b/src/main/java/com/impactupgrade/nucleus/client/SfdcClient.java index 98b700c49..8e2de3687 100644 --- a/src/main/java/com/impactupgrade/nucleus/client/SfdcClient.java +++ b/src/main/java/com/impactupgrade/nucleus/client/SfdcClient.java @@ -824,6 +824,13 @@ public Optional getUserByEmail(String email, String... extraFields) thr return querySingle(query); } + protected static final String TASK_FIELDS = "Id, WhoId, OwnerId, Subject, description, status, priority, activityDate"; + + public Optional getActivityByExternalReference(String externalReference) throws ConnectionException, InterruptedException { + String query = "select " + TASK_FIELDS + " from task where " + env.getConfig().salesforce.fieldDefinitions.activityExternalReference + " = '" + externalReference + "'"; + return querySingle(query); + } + /** * Use with caution, it retrieves ALL active users. Unsuitable for orgs with many users. */ diff --git a/src/main/java/com/impactupgrade/nucleus/controller/MBTController.java b/src/main/java/com/impactupgrade/nucleus/controller/MBTController.java new file mode 100644 index 000000000..ed85444b0 --- /dev/null +++ b/src/main/java/com/impactupgrade/nucleus/controller/MBTController.java @@ -0,0 +1,131 @@ +package com.impactupgrade.nucleus.controller; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.impactupgrade.nucleus.entity.JobStatus; +import com.impactupgrade.nucleus.entity.JobType; +import com.impactupgrade.nucleus.environment.Environment; +import com.impactupgrade.nucleus.environment.EnvironmentFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; + +import static com.impactupgrade.nucleus.service.logic.ActivityService.ActivityType.SMS; + +/** + * To receive webhooks from MBT as messages are sent/received. + */ +@Path("/mbt") +public class MBTController { + + private static final String DATE_FORMAT = "yyyy-MM-dd"; + private static final String DATE_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; + + protected final EnvironmentFactory envFactory; + + public MBTController(EnvironmentFactory envFactory) { + this.envFactory = envFactory; + } + + /** + * The Inbound Messages webhook is triggered by receipt of a message to your MBT account. + */ + @Path("/inbound/sms/webhook") + @POST + @Consumes(MediaType.APPLICATION_JSON) + public Response inboundSmsWebhook( + InboundMessageWebhookData inboundMessageWebhookData, + @Context HttpServletRequest request + ) throws Exception { + Environment env = envFactory.init(request); + + String jobName = "SMS Inbound"; + env.startJobLog(JobType.EVENT, null, jobName, "MBT"); + + // Using combination of subscriber number and today's date + // as a conversation id + // to group all user's messages for current day + String conversationId = inboundMessageWebhookData.subscriberNo + "::" + new SimpleDateFormat(DATE_FORMAT).format(new Date()); + env.activityService().upsertActivity( + SMS, + conversationId, // TODO: use customParams to contain conversation id? + inboundMessageWebhookData.externalReferenceId, + inboundMessageWebhookData.message); + + env.endJobLog(JobStatus.DONE); + + return Response.ok().build(); + } + + /** + * The Message Status webhook is triggered as a message sent from an Account progresses to a Subscriber. + */ + @Path("/sms/status") + @POST + @Consumes(MediaType.APPLICATION_JSON) + public Response smsStatusWebhook( + MessageStatusWebhookData messageStatusWebhookData, + @Context HttpServletRequest request + ) throws Exception { + Environment env = envFactory.init(request); + + String jobName = "SMS Status"; + env.startJobLog(JobType.EVENT, null, jobName, "MBT"); + + // Using combination of msisdn and today's date + // as a conversation id + // to group all user's messages' statuses for current day + String conversationId = messageStatusWebhookData.msisdn + "::" + new SimpleDateFormat(DATE_FORMAT).format(new Date()); + env.activityService().upsertActivity( + SMS, + conversationId, // TODO: use customParams to contain conversation id? + messageStatusWebhookData.messageId, + messageStatusWebhookData.message); + + env.endJobLog(JobStatus.DONE); + + return Response.ok().build(); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class InboundMessageWebhookData { + public String externalReferenceId; + public String type; + public String message; + public String subscriberNo; + public String groupName; + public String groupId; + public String communicationCode; + public String messageType; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_TIME_FORMAT) + public Date receivedTime; + // Every message received sends the data shown in sample to the target URL. + // The customParams parameters may be specified and will be implemented by MBT. + public Map customParams; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class MessageStatusWebhookData { + public String accountId; + public String message; + public String msisdn; + public String groupName; + public String groupId; + public String communicationCode; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = DATE_TIME_FORMAT) + public Date deliveredTime; + public Map properties; + public String statusCode; + public String statusCodeDescription; + public String messageId; + public String referenceId; + } +} diff --git a/src/main/java/com/impactupgrade/nucleus/controller/MailchimpController.java b/src/main/java/com/impactupgrade/nucleus/controller/MailchimpController.java index d4502dc99..f5c05d9f1 100644 --- a/src/main/java/com/impactupgrade/nucleus/controller/MailchimpController.java +++ b/src/main/java/com/impactupgrade/nucleus/controller/MailchimpController.java @@ -1,5 +1,9 @@ package com.impactupgrade.nucleus.controller; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.impactupgrade.nucleus.entity.JobStatus; +import com.impactupgrade.nucleus.entity.JobType; import com.impactupgrade.nucleus.environment.Environment; import com.impactupgrade.nucleus.environment.EnvironmentFactory; @@ -11,13 +15,21 @@ import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.Date; +import java.util.List; + +import static com.impactupgrade.nucleus.service.logic.ActivityService.ActivityType.EMAIL; @Path("/mailchimp") public class MailchimpController { + private static final String DATE_FORMAT = "yyyy-MM-dd"; + protected final EnvironmentFactory envFactory; - public MailchimpController(EnvironmentFactory envFactory){ + public MailchimpController(EnvironmentFactory envFactory) { this.envFactory = envFactory; } @@ -30,7 +42,7 @@ public Response webhook( @FormParam("email") String email, @FormParam("list_id") String listId, @Context HttpServletRequest request - ) throws Exception{ + ) throws Exception { Environment env = envFactory.init(request); env.logJobInfo("action = {} reason = {} email = {} list_id = {}", action, reason, email, listId); @@ -43,4 +55,107 @@ public Response webhook( return Response.status(200).build(); } + + // Message events + @Path("/webhook/message") + @POST + @Consumes(MediaType.APPLICATION_JSON) + public Response messageEvent( + MessageWebhookPayload webhookPayload, + @Context HttpServletRequest request + ) throws Exception { + Environment env = envFactory.init(request); + + String jobName = "Mailchimp webhook events batch"; + env.startJobLog(JobType.EVENT, null, jobName, "Mailchimp"); + + env.logJobInfo("Mailchimp message event batch received. Batch size: {}", webhookPayload.events.size()); + + JobStatus jobStatus = JobStatus.DONE; + for (Event event: webhookPayload.events) { + try { + processEvent(event, env); + } catch (Exception e) { + env.logJobError("Failed to process event! Event type/email: {}/{}", + event.eventType, event.message.email, e); + } + } + + env.endJobLog(jobStatus); + + return Response.status(200).build(); + } + + private void processEvent(Event event, Environment env) throws Exception { + if (event == null) { + return; + } + if ("send".equalsIgnoreCase(event.eventType)) { + // Using sender::recipient::sent-date + // as a conversation id + Date sentAt = Date.from(Instant.ofEpochSecond(event.message.timestamp)); + String conversationId = event.message.sender + "::" + event.message.email + "::" + new SimpleDateFormat(DATE_FORMAT).format(sentAt); + env.activityService().upsertActivity( + EMAIL, + conversationId, + event.message.id, + event.message.subject); // using subject instead of message body (body n\a in the webhook's payload) + } else { + env.logJobInfo("skipping event type {}...", event.eventType); + } + } + + public static final class MessageWebhookPayload { + @JsonProperty("mandrill_events") + public List events; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Event { + @JsonProperty("_id") + public String id; + @JsonProperty("event") + public String eventType; + @JsonProperty("msg") + public Message message; + + //@JsonProperty("ts") + //public Long timestamp; + //public String url; + //public String ip; + //@JsonProperty("user_agent") + //public String userAgent; + //public Object location; + //@JsonProperty("user_agent_parsed") + //public List userAgentParsed; + + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class Message { + @JsonProperty("_id") + public String id; + public String state; // One of: sent, rejected, spam, unsub, bounced, or soft-bounced + public String email; + public String sender; + public String subject; + @JsonProperty("ts") + public Long timestamp; // the integer UTC UNIX timestamp when the message was sent + //TODO: add object definitions, if needed + //@JsonProperty("smtp_events") + //public List smtpEvents; + //public List opens; + //public List clicks; + //public List tags; + //public Map metadata; + //@JsonProperty("subaccount") + //public String subAccount; + //public String diag; //specific SMTP response code and bounce description, if any, received from the remote server + //@JsonProperty("bounce_description") + //public String bounceDescription; + //public String template; + } + + //TODO: Sync events: add/remove (to either of allowlist or denylist) + //TODO: inbound messages } diff --git a/src/main/java/com/impactupgrade/nucleus/controller/TwilioController.java b/src/main/java/com/impactupgrade/nucleus/controller/TwilioController.java index 8c197705b..d2974a95a 100644 --- a/src/main/java/com/impactupgrade/nucleus/controller/TwilioController.java +++ b/src/main/java/com/impactupgrade/nucleus/controller/TwilioController.java @@ -11,6 +11,7 @@ import com.impactupgrade.nucleus.model.ContactSearch; import com.impactupgrade.nucleus.model.CrmContact; import com.impactupgrade.nucleus.model.CrmOpportunity; +import com.impactupgrade.nucleus.security.SecurityUtil; import com.impactupgrade.nucleus.service.logic.NotificationService; import com.impactupgrade.nucleus.util.Utils; import com.twilio.twiml.MessagingResponse; @@ -43,6 +44,7 @@ import static com.impactupgrade.nucleus.entity.JobStatus.DONE; import static com.impactupgrade.nucleus.entity.JobStatus.FAILED; +import static com.impactupgrade.nucleus.service.logic.ActivityService.ActivityType.SMS; import static com.impactupgrade.nucleus.util.Utils.noWhitespace; import static com.impactupgrade.nucleus.util.Utils.trim; @@ -291,6 +293,51 @@ public Response proxyVoice( return Response.ok().entity(xml).build(); } + /** + * This webhook handles 'onMessageAdded' event for Conversations, creating CRM activities. However, note that + * tracking of one-off messages is instead handled by inboundWebhook! + */ + @Path("/callback/conversations") + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + @Produces(MediaType.APPLICATION_JSON) + public Response conversationsWebhook( + @FormParam("EventType") String eventType, + @FormParam("ConversationSid") String conversationSid, + @FormParam("MessageSid") String messageSid, + @FormParam("MessagingServiceSid") String messagingServiceSid, + @FormParam("Index") Integer index, + @FormParam("DateCreated") String date, //ISO8601 time + @FormParam("Body") String body, + @FormParam("Author") String author, + @FormParam("ParticipantSid") String participantSid, + @FormParam("Attributes") String attributes, + @FormParam("Media") String media, // Stringified JSON array of attached media objects + @Context HttpServletRequest request + ) throws Exception { + Environment env = envFactory.init(request); + SecurityUtil.verifyApiKey(env); + + env.startJobLog(JobType.EVENT, null, "Conversation Webhook", "Twilio"); + env.logJobInfo("eventType={} conversationSid={} messageSid={} messagingServiceSid={} index={} date={} body={} author={} participantSid={} attributes={} media={}", + eventType, conversationSid, messageSid, messagingServiceSid, index, date, body, author, participantSid, attributes, media); + + switch (eventType) { + case "onMessageAdded": + env.activityService().upsertActivity( + SMS, + conversationSid, + messageSid, + body); + env.endJobLog(DONE); + return Response.ok().build(); + default: + env.logJobWarn("unexpected eventType: " + eventType); + env.endJobLog(FAILED); + return Response.status(422).build(); + } + } + // TODO: Temporary method to prototype an MMS replacement of the mobile app. In the future, // this can be molded into an API... public static void main(String[] args) { diff --git a/src/main/java/com/impactupgrade/nucleus/controller/TwilioFrontlineController.java b/src/main/java/com/impactupgrade/nucleus/controller/TwilioFrontlineController.java index 200ed915b..95c1ef0ca 100644 --- a/src/main/java/com/impactupgrade/nucleus/controller/TwilioFrontlineController.java +++ b/src/main/java/com/impactupgrade/nucleus/controller/TwilioFrontlineController.java @@ -335,7 +335,7 @@ public Response conversationsCallback( return Response.ok().build(); default: - env.logJobError("unexpected eventType: " + eventType); + env.logJobWarn("unexpected eventType: " + eventType); return Response.status(422).build(); } } diff --git a/src/main/java/com/impactupgrade/nucleus/environment/Environment.java b/src/main/java/com/impactupgrade/nucleus/environment/Environment.java index 9cba6a40f..7b8bff7e2 100644 --- a/src/main/java/com/impactupgrade/nucleus/environment/Environment.java +++ b/src/main/java/com/impactupgrade/nucleus/environment/Environment.java @@ -15,6 +15,7 @@ import com.impactupgrade.nucleus.entity.JobStatus; import com.impactupgrade.nucleus.entity.JobType; import com.impactupgrade.nucleus.service.logic.AccountingService; +import com.impactupgrade.nucleus.service.logic.ActivityService; import com.impactupgrade.nucleus.service.logic.ContactService; import com.impactupgrade.nucleus.service.logic.DonationService; import com.impactupgrade.nucleus.service.logic.MessagingService; @@ -131,6 +132,7 @@ public void setOtherContext(MultivaluedMap otherContext) { // logic services + public ActivityService activityService() { return new ActivityService(this); } public DonationService donationService() { return new DonationService(this); } public ContactService contactService() { return new ContactService(this); } public MessagingService messagingService() { return new MessagingService(this); } diff --git a/src/main/java/com/impactupgrade/nucleus/environment/EnvironmentConfig.java b/src/main/java/com/impactupgrade/nucleus/environment/EnvironmentConfig.java index 21173066e..8589630d0 100644 --- a/src/main/java/com/impactupgrade/nucleus/environment/EnvironmentConfig.java +++ b/src/main/java/com/impactupgrade/nucleus/environment/EnvironmentConfig.java @@ -121,6 +121,8 @@ public static class CRMFieldDefinitions implements Serializable { public String paymentGatewayFailureReason = ""; + public String activityExternalReference = ""; + // donation designation public String fund = ""; diff --git a/src/main/java/com/impactupgrade/nucleus/model/CrmTask.java b/src/main/java/com/impactupgrade/nucleus/model/CrmActivity.java similarity index 63% rename from src/main/java/com/impactupgrade/nucleus/model/CrmTask.java rename to src/main/java/com/impactupgrade/nucleus/model/CrmActivity.java index 5af5a1238..c4840f1b7 100644 --- a/src/main/java/com/impactupgrade/nucleus/model/CrmTask.java +++ b/src/main/java/com/impactupgrade/nucleus/model/CrmActivity.java @@ -2,30 +2,41 @@ import java.util.Calendar; -public class CrmTask { +public class CrmActivity { + public String id; public String targetId; public String assignTo; // User ID public String subject; public String description; + public Type type; public Status status; public Priority priority; public Calendar dueDate; - public CrmTask() { + public CrmActivity() { } - public CrmTask(String targetId, String assignTo, String subject, String description, - Status status, Priority priority, Calendar dueDate) { + public CrmActivity(String targetId, String assignTo, String subject, String description, Type type, Status status, + Priority priority, Calendar dueDate) { this.targetId = targetId; this.assignTo = assignTo; this.subject = subject; this.description = description; + this.type = type; this.status = status; this.priority = priority; this.dueDate = dueDate; } + public enum Type { + TASK, + EMAIL, + LIST_EMAIL, + CADENCE, + CALL + } + public enum Status { TO_DO, IN_PROGRESS, diff --git a/src/main/java/com/impactupgrade/nucleus/service/logic/ActivityService.java b/src/main/java/com/impactupgrade/nucleus/service/logic/ActivityService.java new file mode 100644 index 000000000..dcf25c52e --- /dev/null +++ b/src/main/java/com/impactupgrade/nucleus/service/logic/ActivityService.java @@ -0,0 +1,51 @@ +package com.impactupgrade.nucleus.service.logic; + +import com.google.common.base.Strings; +import com.impactupgrade.nucleus.environment.Environment; +import com.impactupgrade.nucleus.model.CrmActivity; +import com.impactupgrade.nucleus.service.segment.CrmService; + +import java.util.Optional; + +public class ActivityService { + + private final Environment env; + private final CrmService crmService; + + public ActivityService(Environment env) { + this.env = env; + crmService = env.primaryCrmService(); + } + + public enum ActivityType { + EMAIL, SMS + } + + public void upsertActivity(ActivityType activityType, String conversationId, String messageSid, String messageBody) throws Exception { + String subject = activityType.name() + " CONVERSATION: " + conversationId; + + Optional _crmTask = crmService.getActivityByExternalRef(subject); + CrmActivity crmActivity; + + if (_crmTask.isEmpty()) { + crmActivity = new CrmActivity(); + crmActivity.subject = subject; + crmActivity.type = CrmActivity.Type.CALL; + crmActivity.status = CrmActivity.Status.DONE; + crmActivity.priority = CrmActivity.Priority.MEDIUM; + } else { + crmActivity = _crmTask.get(); + } + + if (!Strings.isNullOrEmpty(crmActivity.description)) { + crmActivity.description += "\n"; + } + crmActivity.description += messageSid + ": " + messageBody; + + if (Strings.isNullOrEmpty(crmActivity.id)) { + crmService.insertActivity(crmActivity); + } else { + crmService.updateActivity(crmActivity); + } + } +} diff --git a/src/main/java/com/impactupgrade/nucleus/service/logic/MessagingService.java b/src/main/java/com/impactupgrade/nucleus/service/logic/MessagingService.java index ffeff7cc6..8ff57b345 100644 --- a/src/main/java/com/impactupgrade/nucleus/service/logic/MessagingService.java +++ b/src/main/java/com/impactupgrade/nucleus/service/logic/MessagingService.java @@ -13,6 +13,7 @@ import com.impactupgrade.nucleus.util.Utils; import com.twilio.exception.ApiException; import com.twilio.rest.api.v2010.account.Message; + import javax.ws.rs.core.MultivaluedMap; import java.util.Collections; import java.util.Objects; diff --git a/src/main/java/com/impactupgrade/nucleus/service/logic/NotificationService.java b/src/main/java/com/impactupgrade/nucleus/service/logic/NotificationService.java index cdd55a94c..8f100bc39 100644 --- a/src/main/java/com/impactupgrade/nucleus/service/logic/NotificationService.java +++ b/src/main/java/com/impactupgrade/nucleus/service/logic/NotificationService.java @@ -3,7 +3,7 @@ import com.google.common.base.Strings; import com.impactupgrade.nucleus.environment.Environment; import com.impactupgrade.nucleus.environment.EnvironmentConfig; -import com.impactupgrade.nucleus.model.CrmTask; +import com.impactupgrade.nucleus.model.CrmActivity; import com.impactupgrade.nucleus.service.segment.CrmService; import org.apache.commons.collections.CollectionUtils; @@ -93,10 +93,10 @@ protected void createCrmTask(Notification notification, String targetId, Environ env.logJobInfo("attaching a Task CRM notification to {} and assigning to {}", targetId, assignTo); - crmService.insertTask(new CrmTask( + crmService.insertActivity(new CrmActivity( targetId, assignTo, notification.taskSubject, notification.taskBody, - CrmTask.Status.TO_DO, CrmTask.Priority.MEDIUM, dueDate + CrmActivity.Type.TASK, CrmActivity.Status.TO_DO, CrmActivity.Priority.MEDIUM, dueDate )); } diff --git a/src/main/java/com/impactupgrade/nucleus/service/segment/BasicCrmService.java b/src/main/java/com/impactupgrade/nucleus/service/segment/BasicCrmService.java index c17802930..32662efd0 100644 --- a/src/main/java/com/impactupgrade/nucleus/service/segment/BasicCrmService.java +++ b/src/main/java/com/impactupgrade/nucleus/service/segment/BasicCrmService.java @@ -8,7 +8,7 @@ import com.impactupgrade.nucleus.model.CrmImportEvent; import com.impactupgrade.nucleus.model.CrmOpportunity; import com.impactupgrade.nucleus.model.CrmRecurringDonation; -import com.impactupgrade.nucleus.model.CrmTask; +import com.impactupgrade.nucleus.model.CrmActivity; import com.impactupgrade.nucleus.model.CrmUser; import com.impactupgrade.nucleus.model.ManageDonationEvent; @@ -113,7 +113,15 @@ default Optional getUserByEmail(String email) throws Exception { return Optional.empty(); } - default String insertTask(CrmTask crmTask) throws Exception { + default String insertActivity(CrmActivity crmActivity) throws Exception { + return null; + } + + default String updateActivity(CrmActivity crmActivity) throws Exception { + return null; + } + + default Optional getActivityByExternalRef(String externalRef) throws Exception { return null; } diff --git a/src/main/java/com/impactupgrade/nucleus/service/segment/BloomerangCrmService.java b/src/main/java/com/impactupgrade/nucleus/service/segment/BloomerangCrmService.java index 7f949a931..d809a0955 100644 --- a/src/main/java/com/impactupgrade/nucleus/service/segment/BloomerangCrmService.java +++ b/src/main/java/com/impactupgrade/nucleus/service/segment/BloomerangCrmService.java @@ -23,7 +23,7 @@ import com.impactupgrade.nucleus.model.CrmImportEvent; import com.impactupgrade.nucleus.model.CrmOpportunity; import com.impactupgrade.nucleus.model.CrmRecurringDonation; -import com.impactupgrade.nucleus.model.CrmTask; +import com.impactupgrade.nucleus.model.CrmActivity; import com.impactupgrade.nucleus.model.CrmUser; import com.impactupgrade.nucleus.model.ManageDonationEvent; import com.impactupgrade.nucleus.model.PagedResults; @@ -544,11 +544,21 @@ public Optional getUserByEmail(String email) throws Exception { } @Override - public String insertTask(CrmTask crmTask) throws Exception { + public String insertActivity(CrmActivity crmActivity) throws Exception { // Unlikely to be relevant for Bloomerang. return null; } + @Override + public String updateActivity(CrmActivity crmActivity) throws Exception { + return null; + } + + @Override + public Optional getActivityByExternalRef(String externalRef) throws Exception { + return Optional.empty(); + } + @Override public List insertCustomFields(List crmCustomFields) { return null; diff --git a/src/main/java/com/impactupgrade/nucleus/service/segment/CrmService.java b/src/main/java/com/impactupgrade/nucleus/service/segment/CrmService.java index b7e49e12b..6fde78ff2 100644 --- a/src/main/java/com/impactupgrade/nucleus/service/segment/CrmService.java +++ b/src/main/java/com/impactupgrade/nucleus/service/segment/CrmService.java @@ -15,7 +15,7 @@ import com.impactupgrade.nucleus.model.CrmImportEvent; import com.impactupgrade.nucleus.model.CrmOpportunity; import com.impactupgrade.nucleus.model.CrmRecurringDonation; -import com.impactupgrade.nucleus.model.CrmTask; +import com.impactupgrade.nucleus.model.CrmActivity; import com.impactupgrade.nucleus.model.CrmUser; import com.impactupgrade.nucleus.model.ManageDonationEvent; import com.impactupgrade.nucleus.model.PagedResults; @@ -223,7 +223,9 @@ default void batchFlush() throws Exception { ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // MISC ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - String insertTask(CrmTask crmTask) throws Exception; + String insertActivity(CrmActivity crmActivity) throws Exception; + String updateActivity(CrmActivity crmActivity) throws Exception; + Optional getActivityByExternalRef(String externalRef) throws Exception; List insertCustomFields(List crmCustomFields); diff --git a/src/main/java/com/impactupgrade/nucleus/service/segment/HubSpotCrmService.java b/src/main/java/com/impactupgrade/nucleus/service/segment/HubSpotCrmService.java index 75225e1a4..39fe38018 100644 --- a/src/main/java/com/impactupgrade/nucleus/service/segment/HubSpotCrmService.java +++ b/src/main/java/com/impactupgrade/nucleus/service/segment/HubSpotCrmService.java @@ -51,7 +51,7 @@ import com.impactupgrade.nucleus.model.CrmImportEvent; import com.impactupgrade.nucleus.model.CrmOpportunity; import com.impactupgrade.nucleus.model.CrmRecurringDonation; -import com.impactupgrade.nucleus.model.CrmTask; +import com.impactupgrade.nucleus.model.CrmActivity; import com.impactupgrade.nucleus.model.CrmUser; import com.impactupgrade.nucleus.model.ManageDonationEvent; import com.impactupgrade.nucleus.model.PagedResults; @@ -232,13 +232,23 @@ public Optional getUserByEmail(String id) throws Exception { } @Override - public String insertTask(CrmTask crmTask) throws Exception { + public String insertActivity(CrmActivity crmActivity) throws Exception { EngagementRequest engagementRequest = new EngagementRequest(); - setTaskFields(engagementRequest, crmTask); + setTaskFields(engagementRequest, crmActivity); EngagementRequest response = engagementClient.insert(engagementRequest); return response == null ? null : response.getId() + ""; } + @Override + public String updateActivity(CrmActivity crmActivity) throws Exception { + return null; + } + + @Override + public Optional getActivityByExternalRef(String externalRef) throws Exception { + return Optional.empty(); + } + @Override public List insertCustomFields(List crmCustomFields) { Map> propertiesByObjectTypes = crmCustomFields.stream() @@ -274,32 +284,32 @@ public EnvironmentConfig.CRMFieldDefinitions getFieldDefinitions() { return this.env.getConfig().hubspot.fieldDefinitions; } - protected void setTaskFields(EngagementRequest engagementRequest, CrmTask crmTask) { + protected void setTaskFields(EngagementRequest engagementRequest, CrmActivity crmActivity) { Engagement engagement = new Engagement(); engagement.setActive(true); engagement.setType("TASK"); - engagement.setOwnerId(crmTask.assignTo); + engagement.setOwnerId(crmActivity.assignTo); engagementRequest.setEngagement(engagement); EngagementAssociations associations = new EngagementAssociations(); try { - Long contactId = Long.parseLong(crmTask.targetId); + Long contactId = Long.parseLong(crmActivity.targetId); associations.setContactIds(List.of(contactId)); } catch (NumberFormatException nfe) { - throw new IllegalArgumentException("Failed to parse contact id from target id " + crmTask.targetId + " !"); + throw new IllegalArgumentException("Failed to parse contact id from target id " + crmActivity.targetId + " !"); } engagementRequest.setAssociations(associations); EngagementTaskMetadata metadata = new EngagementTaskMetadata(); - metadata.setBody(crmTask.description); + metadata.setBody(crmActivity.description); - switch (crmTask.status) { + switch (crmActivity.status) { case TO_DO -> metadata.setStatus(EngagementTaskMetadata.Status.NOT_STARTED); case IN_PROGRESS -> metadata.setStatus(EngagementTaskMetadata.Status.IN_PROGRESS); case DONE -> metadata.setStatus(EngagementTaskMetadata.Status.COMPLETED); default -> metadata.setStatus(EngagementTaskMetadata.Status.NOT_STARTED); } - metadata.setSubject(crmTask.subject); + metadata.setSubject(crmActivity.subject); engagementRequest.setMetadata(metadata); } diff --git a/src/main/java/com/impactupgrade/nucleus/service/segment/NoOpCrmService.java b/src/main/java/com/impactupgrade/nucleus/service/segment/NoOpCrmService.java index 5fd9708d7..05d5bcbd4 100644 --- a/src/main/java/com/impactupgrade/nucleus/service/segment/NoOpCrmService.java +++ b/src/main/java/com/impactupgrade/nucleus/service/segment/NoOpCrmService.java @@ -11,7 +11,7 @@ import com.impactupgrade.nucleus.model.CrmImportEvent; import com.impactupgrade.nucleus.model.CrmOpportunity; import com.impactupgrade.nucleus.model.CrmRecurringDonation; -import com.impactupgrade.nucleus.model.CrmTask; +import com.impactupgrade.nucleus.model.CrmActivity; import com.impactupgrade.nucleus.model.CrmUser; import com.impactupgrade.nucleus.model.ManageDonationEvent; import com.impactupgrade.nucleus.model.PagedResults; @@ -224,10 +224,20 @@ public Optional getUserByEmail(String email) throws Exception { } @Override - public String insertTask(CrmTask crmTask) throws Exception { + public String insertActivity(CrmActivity crmActivity) throws Exception { return null; } + @Override + public String updateActivity(CrmActivity crmActivity) throws Exception { + return null; + } + + @Override + public Optional getActivityByExternalRef(String externalRef) throws Exception { + return Optional.empty(); + } + @Override public List insertCustomFields(List crmCustomFields) { return Collections.emptyList(); diff --git a/src/main/java/com/impactupgrade/nucleus/service/segment/SfdcCrmService.java b/src/main/java/com/impactupgrade/nucleus/service/segment/SfdcCrmService.java index de072668e..c7db7c55e 100644 --- a/src/main/java/com/impactupgrade/nucleus/service/segment/SfdcCrmService.java +++ b/src/main/java/com/impactupgrade/nucleus/service/segment/SfdcCrmService.java @@ -24,7 +24,7 @@ import com.impactupgrade.nucleus.model.CrmOpportunity; import com.impactupgrade.nucleus.model.CrmRecord; import com.impactupgrade.nucleus.model.CrmRecurringDonation; -import com.impactupgrade.nucleus.model.CrmTask; +import com.impactupgrade.nucleus.model.CrmActivity; import com.impactupgrade.nucleus.model.CrmUser; import com.impactupgrade.nucleus.model.ManageDonationEvent; import com.impactupgrade.nucleus.model.PagedResults; @@ -232,12 +232,65 @@ public Optional getUserByEmail(String email) throws Exception { } @Override - public String insertTask(CrmTask crmTask) throws Exception { + public String insertActivity(CrmActivity crmActivity) throws Exception { SObject task = new SObject("Task"); - setTaskFields(task, crmTask); + setTaskFields(task, crmActivity); return sfdcClient.insert(task).getId(); } + @Override + public String updateActivity(CrmActivity crmActivity) throws Exception { + SObject task = new SObject("Task"); + if (crmActivity.id != null) { + task.setField("Id", crmActivity.id); + } + setTaskFields(task, crmActivity); + return sfdcClient.update(task).getId(); + } + + @Override + public Optional getActivityByExternalRef(String externalRef) throws Exception { + Optional sObjectO = sfdcClient.getActivityByExternalReference(externalRef); + CrmActivity crmActivity = sObjectO.map(this::toCrmTask).orElse(null); + return Optional.ofNullable(crmActivity); + } + + protected CrmActivity toCrmTask(SObject sObject) { + CrmActivity crmActivity = new CrmActivity( + //sObject.getField("WhoId").toString(), + null, + sObject.getField("OwnerId").toString(), + sObject.getField("Subject").toString(), + sObject.getField("Description").toString(), + null, + null, + null, + null); + + switch (sObject.getField("TaskSubType") + "") { + case "Task" -> crmActivity.type = CrmActivity.Type.TASK; + case "Email" -> crmActivity.type = CrmActivity.Type.EMAIL; + case "List Email" -> crmActivity.type = CrmActivity.Type.LIST_EMAIL; + case "Cadence" -> crmActivity.type = CrmActivity.Type.CADENCE; + default -> crmActivity.type = CrmActivity.Type.CALL; + } + + switch (sObject.getField("Status") + "") { + case "In Progress" -> crmActivity.status = CrmActivity.Status.IN_PROGRESS; + case "Completed" -> crmActivity.status = CrmActivity.Status.DONE; + default -> crmActivity.status = CrmActivity.Status.TO_DO; + } + + switch (sObject.getField("Priority") + "") { + case "Low" -> crmActivity.priority = CrmActivity.Priority.LOW; + case "High" -> crmActivity.priority = CrmActivity.Priority.HIGH; + default -> crmActivity.priority = CrmActivity.Priority.MEDIUM; + } + + crmActivity.id = sObject.getId(); + return crmActivity; + } + @Override public List insertCustomFields(List crmCustomFields) { try { @@ -289,25 +342,33 @@ public EnvironmentConfig.CRMFieldDefinitions getFieldDefinitions() { return this.env.getConfig().salesforce.fieldDefinitions; } - protected void setTaskFields(SObject task, CrmTask crmTask) { - task.setField("WhoId", crmTask.targetId); - task.setField("OwnerId", crmTask.assignTo); - task.setField("Subject", crmTask.subject); - task.setField("Description", crmTask.description); + protected void setTaskFields(SObject task, CrmActivity crmActivity) { + task.setField("WhoId", crmActivity.targetId); + task.setField("OwnerId", crmActivity.assignTo); + task.setField("Subject", crmActivity.subject); + task.setField("Description", crmActivity.description); + + switch (crmActivity.type) { + case TASK -> task.setField("TaskSubType", "Task"); + case EMAIL -> task.setField("TaskSubType", "Email"); + case LIST_EMAIL -> task.setField("TaskSubType", "List Email"); + case CADENCE -> task.setField("TaskSubType", "Cadence"); + default -> task.setField("TaskSubType", "Call"); + } - switch (crmTask.status) { + switch (crmActivity.status) { case IN_PROGRESS -> task.setField("Status", "In Progress"); case DONE -> task.setField("Status", "Completed"); default -> task.setField("Status", "Not Started"); } - switch (crmTask.priority) { + switch (crmActivity.priority) { case LOW -> task.setField("Priority", "Low"); case HIGH, CRITICAL -> task.setField("Priority", "High"); default -> task.setField("Priority", "Normal"); } - task.setField("ActivityDate", crmTask.dueDate); + task.setField("ActivityDate", crmActivity.dueDate); } @Override diff --git a/src/main/java/com/impactupgrade/nucleus/service/segment/SharePointCrmService.java b/src/main/java/com/impactupgrade/nucleus/service/segment/SharePointCrmService.java index 73512714c..19c6b5d50 100644 --- a/src/main/java/com/impactupgrade/nucleus/service/segment/SharePointCrmService.java +++ b/src/main/java/com/impactupgrade/nucleus/service/segment/SharePointCrmService.java @@ -16,7 +16,7 @@ import com.impactupgrade.nucleus.model.CrmImportEvent; import com.impactupgrade.nucleus.model.CrmOpportunity; import com.impactupgrade.nucleus.model.CrmRecurringDonation; -import com.impactupgrade.nucleus.model.CrmTask; +import com.impactupgrade.nucleus.model.CrmActivity; import com.impactupgrade.nucleus.model.CrmUser; import com.impactupgrade.nucleus.model.ManageDonationEvent; import com.impactupgrade.nucleus.model.PagedResults; @@ -423,7 +423,7 @@ public Optional getUserByEmail(String email) throws Exception { } @Override - public String insertTask(CrmTask crmTask) throws Exception { + public String insertActivity(CrmActivity crmActivity) throws Exception { return null; } @@ -432,6 +432,16 @@ public List insertCustomFields(List crmCustomFie return null; } + @Override + public String updateActivity(CrmActivity crmActivity) throws Exception { + return null; + } + + @Override + public Optional getActivityByExternalRef(String externalRef) throws Exception { + return Optional.empty(); + } + @Override public EnvironmentConfig.CRMFieldDefinitions getFieldDefinitions() { return null; diff --git a/src/main/java/com/impactupgrade/nucleus/service/segment/VirtuousCrmService.java b/src/main/java/com/impactupgrade/nucleus/service/segment/VirtuousCrmService.java index 879ce7af8..a2437e69f 100644 --- a/src/main/java/com/impactupgrade/nucleus/service/segment/VirtuousCrmService.java +++ b/src/main/java/com/impactupgrade/nucleus/service/segment/VirtuousCrmService.java @@ -6,6 +6,7 @@ import com.impactupgrade.nucleus.environment.EnvironmentConfig; import com.impactupgrade.nucleus.model.ContactSearch; import com.impactupgrade.nucleus.model.CrmAccount; +import com.impactupgrade.nucleus.model.CrmActivity; import com.impactupgrade.nucleus.model.CrmAddress; import com.impactupgrade.nucleus.model.CrmCampaign; import com.impactupgrade.nucleus.model.CrmContact; @@ -14,7 +15,6 @@ import com.impactupgrade.nucleus.model.CrmImportEvent; import com.impactupgrade.nucleus.model.CrmOpportunity; import com.impactupgrade.nucleus.model.CrmRecurringDonation; -import com.impactupgrade.nucleus.model.CrmTask; import com.impactupgrade.nucleus.model.CrmUser; import com.impactupgrade.nucleus.model.ManageDonationEvent; import com.impactupgrade.nucleus.model.PagedResults; @@ -524,38 +524,50 @@ public Map getFieldOptions(String object) throws Exception { } @Override - public String insertTask(CrmTask crmTask) throws Exception { - VirtuousClient.Task task = asTask(crmTask); + public String insertActivity(CrmActivity crmActivity) throws Exception { + VirtuousClient.Task task = asTask(crmActivity); VirtuousClient.Task createdTask = virtuousClient.createTask(task); return createdTask == null ? null : createdTask.id + ""; } @Override - public List insertCustomFields(List crmCustomFields) { + public String updateActivity(CrmActivity crmActivity) throws Exception { + // TODO: May not be possible? return null; } - private VirtuousClient.Task asTask(CrmTask crmTask) { - if (crmTask == null) { + @Override + public Optional getActivityByExternalRef(String externalRef) throws Exception { + // TODO + return Optional.empty(); + } + + private VirtuousClient.Task asTask(CrmActivity crmActivity) { + if (crmActivity == null) { return null; } VirtuousClient.Task task = new VirtuousClient.Task(); task.taskType = VirtuousClient.Task.Type.GENERAL; - task.task = crmTask.subject; - task.description = crmTask.description; - if (crmTask.dueDate != null) { - task.dueDateTime = new SimpleDateFormat(DATE_TIME_FORMAT).format(crmTask.dueDate.getTime()); + task.task = crmActivity.subject; + task.description = crmActivity.description; + if (crmActivity.dueDate != null) { + task.dueDateTime = new SimpleDateFormat(DATE_TIME_FORMAT).format(crmActivity.dueDate.getTime()); } try { - task.contactId = Integer.parseInt(crmTask.targetId); + task.contactId = Integer.parseInt(crmActivity.targetId); } catch (NumberFormatException e) { env.logJobWarn("Failed to parse Integer from String '{}'!", task.contactId); } - task.contact = crmTask.targetId; + task.contact = crmActivity.targetId; return task; } + @Override + public List insertCustomFields(List crmCustomFields) { + return null; + } + @Override public double getDonationsTotal(String filter) throws Exception { // TODO diff --git a/src/main/resources/environment-sample.json b/src/main/resources/environment-sample.json index 1e2445747..17ea5fd9d 100644 --- a/src/main/resources/environment-sample.json +++ b/src/main/resources/environment-sample.json @@ -21,6 +21,8 @@ "paymentGatewayDepositNetAmount": "Payment_Gateway_Deposit_Net_Amount__c", "paymentGatewayDepositFee": "Payment_Gateway_Deposit_Fee__c", + "activityExternalReference": "Subject", + "fund": "", "emailOptIn": "",