diff --git a/src/main/java/com/impactupgrade/nucleus/App.java b/src/main/java/com/impactupgrade/nucleus/App.java index 58d82d067..e197e2eae 100644 --- a/src/main/java/com/impactupgrade/nucleus/App.java +++ b/src/main/java/com/impactupgrade/nucleus/App.java @@ -11,6 +11,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; @@ -98,6 +99,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"))) { @@ -157,6 +159,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 24784b1f5..b0774794e 100644 --- a/src/main/java/com/impactupgrade/nucleus/client/SfdcClient.java +++ b/src/main/java/com/impactupgrade/nucleus/client/SfdcClient.java @@ -774,6 +774,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..6dd94a336 --- /dev/null +++ b/src/main/java/com/impactupgrade/nucleus/controller/MBTController.java @@ -0,0 +1,134 @@ +package com.impactupgrade.nucleus.controller; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.impactupgrade.nucleus.entity.JobType; +import com.impactupgrade.nucleus.environment.Environment; +import com.impactupgrade.nucleus.environment.EnvironmentFactory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +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.MessagingService.MessageType.SMS; + +/** + * To receive webhooks from MBT as messages are sent/received. + */ +@Path("/mbt") +public class MBTController { + + private static final Logger log = LogManager.getLogger(MBTController.class); + + 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.messagingService().upsertCrmConversation( + SMS, + conversationId, // TODO: use customParams to contain conversation id? + inboundMessageWebhookData.externalReferenceId, + inboundMessageWebhookData.message); + + env.endJobLog(jobName); + + 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.messagingService().upsertCrmConversation( + SMS, + conversationId, // TODO: use customParams to contain conversation id? + messageStatusWebhookData.messageId, + messageStatusWebhookData.message); + + env.endJobLog(jobName); + + 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 9405bcc9d..04b514c6c 100644 --- a/src/main/java/com/impactupgrade/nucleus/controller/MailchimpController.java +++ b/src/main/java/com/impactupgrade/nucleus/controller/MailchimpController.java @@ -1,5 +1,8 @@ package com.impactupgrade.nucleus.controller; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.impactupgrade.nucleus.entity.JobType; import com.impactupgrade.nucleus.environment.Environment; import com.impactupgrade.nucleus.environment.EnvironmentFactory; import org.apache.logging.log4j.LogManager; @@ -13,15 +16,23 @@ 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.MessagingService.MessageType.EMAIL; @Path("/mailchimp") public class MailchimpController { private static final Logger log = LogManager.getLogger(MailchimpController.class); + private static final String DATE_FORMAT = "yyyy-MM-dd"; + protected final EnvironmentFactory envFactory; - public MailchimpController(EnvironmentFactory envFactory){ + public MailchimpController(EnvironmentFactory envFactory) { this.envFactory = envFactory; } @@ -34,7 +45,7 @@ public Response webhook( @FormParam("email") String email, @FormParam("list_id") String listId, @Context HttpServletRequest request - ) throws Exception{ + ) throws Exception { log.info("action = {} reason = {} email = {} list_id = {}", action, reason, email, listId); Environment env = envFactory.init(request); @@ -46,4 +57,106 @@ 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); + + log.info("Mailchimp message event batch received. Batch size: {}.", webhookPayload.events.size()); + + String jobName = "Mailchimp webhook events batch"; + env.startJobLog(JobType.EVENT, null, jobName, "Mailchimp"); + + for (Event event: webhookPayload.events) { + try { + processEvent(event, env); + } catch (Exception e) { + log.error("Failed to process event! Event type/email: {}/{}; {}", + event.eventType, event.message.email, e); + } + } + + env.endJobLog(jobName); + + 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.messagingService().upsertCrmConversation( + EMAIL, + conversationId, + event.message.id, + event.message.subject); // using subject instead of message body (body n\a in the webhook's payload) + } else { + log.info("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 90b9e7dbe..f1651b1d7 100644 --- a/src/main/java/com/impactupgrade/nucleus/controller/TwilioController.java +++ b/src/main/java/com/impactupgrade/nucleus/controller/TwilioController.java @@ -11,6 +11,8 @@ 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.MessagingService; import com.impactupgrade.nucleus.util.Utils; import com.twilio.twiml.MessagingResponse; import com.twilio.twiml.VoiceResponse; @@ -37,6 +39,7 @@ import java.util.Locale; import java.util.stream.Collectors; +import static com.impactupgrade.nucleus.service.logic.MessagingService.MessageType.SMS; import static com.impactupgrade.nucleus.util.Utils.noWhitespace; import static com.impactupgrade.nucleus.util.Utils.trim; @@ -240,6 +243,45 @@ 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); + + switch (eventType) { + case "onMessageAdded": + env.messagingService().upsertCrmConversation( + SMS, + conversationSid, + messageSid, + body); + return Response.ok().build(); + default: + log.warn("unexpected eventType: " + eventType); + 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 765b2293f..b2ea589f8 100644 --- a/src/main/java/com/impactupgrade/nucleus/controller/TwilioFrontlineController.java +++ b/src/main/java/com/impactupgrade/nucleus/controller/TwilioFrontlineController.java @@ -338,7 +338,7 @@ public Response conversationsCallback( return Response.ok().build(); default: - log.error("unexpected eventType: " + eventType); + log.warn("unexpected eventType: " + eventType); return Response.status(422).build(); } } diff --git a/src/main/java/com/impactupgrade/nucleus/environment/EnvironmentConfig.java b/src/main/java/com/impactupgrade/nucleus/environment/EnvironmentConfig.java index da7c85ad5..977e1142f 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 62% rename from src/main/java/com/impactupgrade/nucleus/model/CrmTask.java rename to src/main/java/com/impactupgrade/nucleus/model/CrmActivity.java index 5af5a1238..57e3c18d3 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/MessagingService.java b/src/main/java/com/impactupgrade/nucleus/service/logic/MessagingService.java index caff578d4..a4539a751 100644 --- a/src/main/java/com/impactupgrade/nucleus/service/logic/MessagingService.java +++ b/src/main/java/com/impactupgrade/nucleus/service/logic/MessagingService.java @@ -8,6 +8,7 @@ import com.impactupgrade.nucleus.client.TwilioClient; import com.impactupgrade.nucleus.environment.Environment; import com.impactupgrade.nucleus.model.ContactSearch; +import com.impactupgrade.nucleus.model.CrmActivity; import com.impactupgrade.nucleus.model.CrmContact; import com.impactupgrade.nucleus.service.segment.CrmService; import com.impactupgrade.nucleus.util.Utils; @@ -17,6 +18,7 @@ import org.apache.logging.log4j.Logger; import java.util.Objects; +import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -34,6 +36,10 @@ public MessagingService(Environment env) { crmService = env.messagingCrmService(); } + public enum MessageType { + EMAIL, SMS; + } + public void sendMessage(String message, CrmContact crmContact, String sender) { try { String pn = crmContact.phoneNumberForSMS(); @@ -230,4 +236,32 @@ public void optOut(CrmContact crmContact) throws Exception { crmContact.smsOptOut = true; crmService.updateContact(crmContact); } + + public void upsertCrmConversation(MessageType messageType, String conversationId, String messageSid, String messageBody) throws Exception { + String subject = messageType.name() + " CONVERSATION: " + conversationId; + + Optional _crmTask = env.primaryCrmService().getTaskByExternalRef(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)) { + env.primaryCrmService().insertActivity(crmActivity); + } else { + env.primaryCrmService().updateActivity(crmActivity); + } + } } 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 20f41bdc8..817f1f1fc 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; import org.apache.logging.log4j.LogManager; @@ -83,10 +83,10 @@ protected void createCrmTask(String subject, String body, String targetId, Envir dueDate.add(Calendar.HOUR, 7 * 24); String assignTo = notificationConfig.assignTo; - crmService.insertTask(new CrmTask( + crmService.insertActivity(new CrmActivity( targetId, assignTo, subject, body, - 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 ee35ad5b6..00bf2549a 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; @@ -109,7 +109,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 getTaskByExternalRef(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 2a5731e38..f8edf0f15 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; @@ -521,11 +521,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 getTaskByExternalRef(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 d13e1344f..a06937ff3 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; @@ -220,7 +220,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 getTaskByExternalRef(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 b018f604d..e8f06bf63 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; @@ -236,13 +236,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 getTaskByExternalRef(String externalRef) throws Exception { + return Optional.empty(); + } + @Override public List insertCustomFields(List crmCustomFields) { Map> propertiesByObjectTypes = crmCustomFields.stream() @@ -278,32 +288,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 337967b45..d479b0c83 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; @@ -213,10 +213,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 getTaskByExternalRef(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 dfb0101fc..ebf4f40ac 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; @@ -230,12 +230,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 getTaskByExternalRef(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 { @@ -287,25 +340,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 26cb5b4ed..f44802002 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; @@ -416,7 +416,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; } @@ -425,6 +425,16 @@ public List insertCustomFields(List crmCustomFie return null; } + @Override + public String updateActivity(CrmActivity crmActivity) throws Exception { + return null; + } + + @Override + public Optional getTaskByExternalRef(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 d0abc38fb..d17eb266c 100644 --- a/src/main/java/com/impactupgrade/nucleus/service/segment/VirtuousCrmService.java +++ b/src/main/java/com/impactupgrade/nucleus/service/segment/VirtuousCrmService.java @@ -8,7 +8,7 @@ import com.impactupgrade.nucleus.model.CrmAddress; import com.impactupgrade.nucleus.model.CrmContact; import com.impactupgrade.nucleus.model.CrmDonation; -import com.impactupgrade.nucleus.model.CrmTask; +import com.impactupgrade.nucleus.model.CrmActivity; import com.impactupgrade.nucleus.model.CrmUser; import com.impactupgrade.nucleus.model.PagedResults; import org.apache.commons.collections.CollectionUtils; @@ -508,30 +508,30 @@ 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 + ""; } - private VirtuousClient.Task asTask(CrmTask crmTask) { - if (crmTask == null) { + 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) { log.warn("Failed to parse Integer from String '{}'!", task.contactId); } - task.contact = crmTask.targetId; + task.contact = crmActivity.targetId; return task; } diff --git a/src/main/resources/environment-sample.json b/src/main/resources/environment-sample.json index 705f33a27..49be9801b 100644 --- a/src/main/resources/environment-sample.json +++ b/src/main/resources/environment-sample.json @@ -28,6 +28,8 @@ "paymentGatewayDepositNetAmount": "Deposit_Net__c", "paymentGatewayDepositFee": "Deposit_Fee__c", + "activityExternalReference": "Subject", + "paymentGatewayFailureReason": "Failure_Reason__c", "fund": "",