Skip to content

Commit

Permalink
refactor(Update to send email to accpet trusted companion):
Browse files Browse the repository at this point in the history
  • Loading branch information
br648 committed Oct 23, 2024
1 parent 0e5ad5d commit bacb87e
Show file tree
Hide file tree
Showing 11 changed files with 204 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package org.opentripplanner.middleware.controllers.api;

import io.github.manusant.ss.ApiEndpoint;
import com.twilio.rest.verify.v2.service.Verification;
import com.twilio.rest.verify.v2.service.VerificationCheck;
import io.github.manusant.ss.ApiEndpoint;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jetty.http.HttpStatus;
import org.opentripplanner.middleware.auth.Auth0Connection;
import org.opentripplanner.middleware.auth.RequestingUser;
import org.opentripplanner.middleware.models.OtpUser;
import org.opentripplanner.middleware.models.RelatedUser;
import org.opentripplanner.middleware.persistence.Persistence;
import org.opentripplanner.middleware.utils.HttpUtils;
import org.opentripplanner.middleware.tripmonitor.TrustedCompanion;
import org.opentripplanner.middleware.utils.JsonUtils;
import org.opentripplanner.middleware.utils.NotificationUtils;
import org.opentripplanner.middleware.utils.SwaggerUtils;
Expand All @@ -25,6 +24,7 @@
import java.util.regex.Pattern;

import static io.github.manusant.ss.descriptor.MethodDescriptor.path;
import static org.opentripplanner.middleware.tripmonitor.TrustedCompanion.manageAcceptDependentEmail;
import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt;

/**
Expand All @@ -35,11 +35,18 @@ public class OtpUserController extends AbstractUserController<OtpUser> {
private static final Logger LOG = LoggerFactory.getLogger(OtpUserController.class);

private static final String CODE_PARAM = "code";

private static final String PHONE_PARAM = "phoneNumber";

private static final String VERIFY_PATH = "verify_sms";

public static final String OTP_USER_PATH = "secure/user";

private static final String VERIFY_ROUTE_TEMPLATE = "/:%s/%s/:%s";
/** Regex to check E.164 phone number format per https://www.twilio.com/docs/glossary/what-e164 */

/**
* Regex to check E.164 phone number format per https://www.twilio.com/docs/glossary/what-e164
*/
private static final Pattern PHONE_E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{1,14}$");

public OtpUserController(String apiPrefix) {
Expand All @@ -58,6 +65,7 @@ OtpUser preCreateHook(OtpUser user, Request req) {
if (Objects.nonNull(user.mobilityProfile)) {
user.mobilityProfile.updateMobilityMode();
}
manageAcceptDependentEmail(user);
return super.preCreateHook(user, req);
}

Expand All @@ -66,6 +74,7 @@ OtpUser preUpdateHook(OtpUser user, OtpUser preExistingUser, Request req) {
if (Objects.nonNull(user.mobilityProfile)) {
user.mobilityProfile.updateMobilityMode();
}
manageAcceptDependentEmail(user);
return super.preUpdateHook(user, preExistingUser, req);
}

Expand All @@ -83,7 +92,7 @@ protected void buildEndpoint(ApiEndpoint baseEndpoint) {
.withRequired(true)
.withDescription("The dependent user id.")
.and(),
OtpUserController::acceptDependent
TrustedCompanion::acceptDependent
)
.get(path(ROOT_ROUTE + String.format(VERIFY_ROUTE_TEMPLATE, ID_PARAM, VERIFY_PATH, PHONE_PARAM))
.withDescription("Request an SMS verification to be sent to an OtpUser's phone number.")
Expand Down Expand Up @@ -195,51 +204,4 @@ public static boolean isPhoneNumberValidE164(String phoneNumber) {
Matcher m = PHONE_E164_PATTERN.matcher(phoneNumber);
return m.matches();
}

/**
* Accept a request from another user to be their dependent. This will include both companions and observers.
*/
private static OtpUser acceptDependent(Request request, Response response) {
RequestingUser requestingUser = Auth0Connection.getUserFromRequest(request);
OtpUser relatedUser = requestingUser.otpUser;
if (relatedUser == null) {
logMessageAndHalt(request, HttpStatus.BAD_REQUEST_400, "Otp user unknown.");
return null;
}

String dependentUserId = HttpUtils.getQueryParamFromRequest(request, USER_ID_PARAM, false);
if (dependentUserId.isEmpty()) {
logMessageAndHalt(request, HttpStatus.BAD_REQUEST_400, "Dependent user id not provided.");
return null;
}

OtpUser dependentUser = Persistence.otpUsers.getById(dependentUserId);
if (dependentUser == null) {
logMessageAndHalt(request, HttpStatus.BAD_REQUEST_400, "Dependent user unknown!");
return null;
}

boolean isRelated = dependentUser.relatedUsers
.stream()
.filter(related -> related.userId.equals(relatedUser.id))
// Update related user status. This assumes a related user with "pending" status was previously added.
.peek(related -> related.status = RelatedUser.RelatedUserStatus.CONFIRMED)
.findFirst()
.isPresent();

if (isRelated) {
// Maintain a list of dependents.
relatedUser.dependents.add(dependentUserId);
Persistence.otpUsers.replace(relatedUser.id, relatedUser);
// Update list of related users.
Persistence.otpUsers.replace(dependentUser.id, dependentUser);
} else {
logMessageAndHalt(request, HttpStatus.BAD_REQUEST_400, "Dependent did not request user to be related!");
return null;
}

// TODO: Not sure what is required in the response. For now, returning the updated related user.
return relatedUser;
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@
* Message.properties.
*/
public enum Message {
ACCEPT_DEPENDENT_EMAIL_FOOTER,
ACCEPT_DEPENDENT_EMAIL_GREETING,
ACCEPT_DEPENDENT_EMAIL_LINK_TEXT,
ACCEPT_DEPENDENT_EMAIL_SUBJECT,
ACCEPT_DEPENDENT_EMAIL_MANAGE,
LABEL_AND_CONTENT,
SMS_STOP_NOTIFICATIONS,
TRIP_EMAIL_SUBJECT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum RelatedUserStatus {
public String userId;
public String email;
public RelatedUserStatus status;
public boolean acceptDependentEmailSent;

public RelatedUser() {
// Required for JSON deserialization.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package org.opentripplanner.middleware.tripmonitor;

import org.eclipse.jetty.http.HttpStatus;
import org.opentripplanner.middleware.auth.Auth0Connection;
import org.opentripplanner.middleware.auth.RequestingUser;
import org.opentripplanner.middleware.i18n.Message;
import org.opentripplanner.middleware.models.OtpUser;
import org.opentripplanner.middleware.models.RelatedUser;
import org.opentripplanner.middleware.persistence.Persistence;
import org.opentripplanner.middleware.utils.ConfigUtils;
import org.opentripplanner.middleware.utils.HttpUtils;
import org.opentripplanner.middleware.utils.I18nUtils;
import org.opentripplanner.middleware.utils.NotificationUtils;
import spark.Request;
import spark.Response;

import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import static org.opentripplanner.middleware.controllers.api.ApiController.USER_ID_PARAM;
import static org.opentripplanner.middleware.tripmonitor.jobs.CheckMonitoredTrip.SETTINGS_PATH;
import static org.opentripplanner.middleware.utils.I18nUtils.label;
import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt;

public class TrustedCompanion {

private TrustedCompanion() {
throw new IllegalStateException("Utility class");
}

private static final String OTP_UI_URL = ConfigUtils.getConfigPropertyAsText("OTP_UI_URL");

public static final String ACCEPT_DEPENDENT_PATH = "api/secure/user/acceptdependent";

/**
* Accept a request from another user to be their dependent. This will include both companions and observers.
*/
public static OtpUser acceptDependent(Request request, Response response) {
RequestingUser requestingUser = Auth0Connection.getUserFromRequest(request);
OtpUser relatedUser = requestingUser.otpUser;
if (relatedUser == null) {
logMessageAndHalt(request, HttpStatus.BAD_REQUEST_400, "Otp user unknown.");
return null;
}

String dependentUserId = HttpUtils.getQueryParamFromRequest(request, USER_ID_PARAM, false);
if (dependentUserId.isEmpty()) {
logMessageAndHalt(request, HttpStatus.BAD_REQUEST_400, "Dependent user id not provided.");
return null;
}

OtpUser dependentUser = Persistence.otpUsers.getById(dependentUserId);
if (dependentUser == null) {
logMessageAndHalt(request, HttpStatus.BAD_REQUEST_400, "Dependent user unknown!");
return null;
}

boolean isRelated = dependentUser.relatedUsers
.stream()
.filter(related -> related.userId.equals(relatedUser.id))
// Update related user status. This assumes a related user with "pending" status was previously added.
.peek(related -> related.status = RelatedUser.RelatedUserStatus.CONFIRMED)
.findFirst()
.isPresent();

if (isRelated) {
// Maintain a list of dependents.
relatedUser.dependents.add(dependentUserId);
Persistence.otpUsers.replace(relatedUser.id, relatedUser);
// Update list of related users.
Persistence.otpUsers.replace(dependentUser.id, dependentUser);
} else {
logMessageAndHalt(request, HttpStatus.BAD_REQUEST_400, "Dependent did not request user to be related!");
return null;
}

// TODO: Not sure what is required in the response. For now, returning the updated related user.
return relatedUser;
}

/**
* When creating or updating an OTP user, extract a list of newly defined dependents and send an 'accept dependent'
* email to each. Then update which dependents have been sent an email so subsequent updates do not trigger
* additional emails.
*/
public static void manageAcceptDependentEmail(OtpUser dependentUser) {
if (dependentUser.relatedUsers.isEmpty()) {
// No related users defined by dependent.
return;
}

dependentUser.relatedUsers
.stream()
.filter(relatedUser -> !relatedUser.acceptDependentEmailSent)
.forEach(relatedUser -> {
OtpUser user = Persistence.otpUsers.getById(relatedUser.userId);
if (user != null && sendAcceptDependentEmail(dependentUser, user)) {
relatedUser.acceptDependentEmailSent = true;
}
});

// Preserve email sent status.
Persistence.otpUsers.replace(dependentUser.id, dependentUser);
}

/**
* Send 'accept dependent' email.
*/
private static boolean sendAcceptDependentEmail(OtpUser dependentUser, OtpUser relatedUser) {
Locale locale = I18nUtils.getOtpUserLocale(relatedUser);

String acceptDependentLinkLabel = Message.ACCEPT_DEPENDENT_EMAIL_LINK_TEXT.get(locale);
String acceptDependentUrl = getAcceptDependentUrl(dependentUser);

// A HashMap is needed instead of a Map for template data to be serialized to the template renderer.
Map<String, Object> templateData = new HashMap<>(Map.of(
"acceptDependentLinkAnchorLabel", acceptDependentLinkLabel,
"acceptDependentLinkLabelAndUrl", label(acceptDependentLinkLabel, acceptDependentUrl, locale),
"acceptDependentUrl", getAcceptDependentUrl(dependentUser),
"emailFooter", Message.ACCEPT_DEPENDENT_EMAIL_FOOTER.get(locale),
"emailGreeting", String.format("%s%s", dependentUser.email, Message.ACCEPT_DEPENDENT_EMAIL_GREETING.get(locale)),
// TODO: This is required in the `OtpUserContainer.ftl` template. Not sure what to link to so providing link back to settings.
"manageLinkUrl", String.format("%s%s", OTP_UI_URL, SETTINGS_PATH),
"manageLinkText", Message.ACCEPT_DEPENDENT_EMAIL_MANAGE.get(locale)
));

return NotificationUtils.sendEmail(
relatedUser,
Message.ACCEPT_DEPENDENT_EMAIL_SUBJECT.get(locale),
"AcceptDependentText.ftl",
"AcceptDependentHtml.ftl",
templateData
);
}

private static String getAcceptDependentUrl(OtpUser dependentUser) {
// TODO: Is OTP_UI_URL the correct base URL to user here? If not, what?!
return String.format("%s%s?userId=%s", OTP_UI_URL, ACCEPT_DEPENDENT_PATH, dependentUser.id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ public class CheckMonitoredTrip implements Runnable {
public static final int MAXIMUM_MONITORED_TRIP_ITINERARY_CHECKS =
ConfigUtils.getConfigPropertyAsInt("MAXIMUM_MONITORED_TRIP_ITINERARY_CHECKS", 3);

private final String ACCOUNT_PATH = "/#/account";
public static final String ACCOUNT_PATH = "/#/account";

private final String TRIPS_PATH = ACCOUNT_PATH + "/trips";

private final String SETTINGS_PATH = ACCOUNT_PATH + "/settings";
public static final String SETTINGS_PATH = ACCOUNT_PATH + "/settings";

public final MonitoredTrip trip;

Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/Message.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
ACCEPT_DEPENDENT_EMAIL_FOOTER = You are receiving this email because you have been selected to be a trusted companion.
ACCEPT_DEPENDENT_EMAIL_GREETING = %s would like you to be their trusted companion.
ACCEPT_DEPENDENT_EMAIL_LINK_TEXT = Accept trusted companion
ACCEPT_DEPENDENT_EMAIL_SUBJECT = Trusted companion request
ACCEPT_DEPENDENT_EMAIL_MANAGE = Manage settings
LABEL_AND_CONTENT = %s: %s
SMS_STOP_NOTIFICATIONS = To stop receiving notifications, reply STOP.
TRIP_EMAIL_SUBJECT = %s Notification
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/Message_fr.properties
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
ACCEPT_DEPENDENT_EMAIL_FOOTER = TODO
ACCEPT_DEPENDENT_EMAIL_GREETING = %s TODO.
ACCEPT_DEPENDENT_EMAIL_LINK_TEXT = TODO
ACCEPT_DEPENDENT_EMAIL_SUBJECT = TODO
ACCEPT_DEPENDENT_EMAIL_MANAGE = TODO
LABEL_AND_CONTENT = %s\u00A0: %s
SMS_STOP_NOTIFICATIONS = Pour arrêter ces notifications, envoyez STOP.
TRIP_EMAIL_SUBJECT = Notification pour %s
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/latest-spark-swagger-output.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2887,6 +2887,8 @@ definitions:
- "PENDING"
- "CONFIRMED"
- "INVALID"
acceptDependentEmailSent:
type: "boolean"
FareComponent:
type: "object"
properties:
Expand Down
15 changes: 15 additions & 0 deletions src/main/resources/templates/AcceptDependentHtml.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<#ftl auto_esc=false>
<#include "OtpUserContainer.ftl">

<#--
This is a template for an HTML email that gets sent when a dependent user is requesting a trusted companion.
-->

<#macro EmailMain>
<div>
<h1>${emailGreeting}</h1>
<p><a href="${acceptDependentUrl}">${acceptDependentLinkAnchorLabel}</a></p>
</div>
</#macro>

<@HtmlEmail/>
11 changes: 11 additions & 0 deletions src/main/resources/templates/AcceptDependentText.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<#--
This is a template for a text email that gets sent when a dependent requests a trusted companion.
Note: in plain text emails, all whitespace is preserved,
so the indentation of the notification content is intentionally not aligned
with the indentation of the macros.
-->
${emailGreeting}

${tripLinkLabelAndUrl}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import static org.opentripplanner.middleware.testutils.ApiTestUtils.createAndAssignAuth0User;
import static org.opentripplanner.middleware.testutils.ApiTestUtils.getMockHeaders;
import static org.opentripplanner.middleware.testutils.ApiTestUtils.makeGetRequest;
Expand All @@ -35,6 +36,7 @@
import static org.opentripplanner.middleware.auth.Auth0Connection.restoreDefaultAuthDisabled;
import static org.opentripplanner.middleware.auth.Auth0Connection.setAuthDisabled;
import static org.opentripplanner.middleware.testutils.PersistenceTestUtils.deleteOtpUser;
import static org.opentripplanner.middleware.tripmonitor.TrustedCompanion.ACCEPT_DEPENDENT_PATH;

public class OtpUserControllerTest extends OtpMiddlewareTestEnvironment {
private static final String INITIAL_PHONE_NUMBER = "+15555550222"; // Fake US 555 number.
Expand All @@ -46,7 +48,6 @@ public class OtpUserControllerTest extends OtpMiddlewareTestEnvironment {
private static OtpUser relatedUserThree;
private static OtpUser dependentUserThree;
private static HashMap<String, String> relatedUserHeaders;
public static final String ACCEPT_DEPENDENT_PATH = "api/secure/user/acceptdependent";

@BeforeAll
public static void setUp() throws Exception {
Expand Down

0 comments on commit bacb87e

Please sign in to comment.