diff --git a/ramls/edge-patron.raml b/ramls/edge-patron.raml index 592dd19..0d45359 100644 --- a/ramls/edge-patron.raml +++ b/ramls/edge-patron.raml @@ -622,50 +622,49 @@ types: body: text/plain: example: internal server error, contact administrator -/patron-registration-status/{emailId}: - uriParameters: - emailId: - description: The email ID of the patron. - type: string - required: true - get: - description: Get the patron details by email ID - queryParameters: - apikey: - description: "API Key" - type: string - responses: - 200: - description: patron information retrieved successfully - body: - application/json: - type: user - example: !include examples/user.json - 400: - description: Validation error - body: - application/json: - type: user_error_400 - example: !include examples/user_error.json - 401: - description: Not authorized to perform requested action - body: - text/plain: - example: unable to get account -- unauthorized - 403: - description: Access Denied - body: - text/plain: - example: Access Denied - 404: - description: Validation error - body: - application/json: - type: user_error_404 - example: !include examples/user_error.json - 500: - description: Internal server error, e.g. due to misconfiguration - body: - text/plain: - example: internal server error, contact administrator + /registration-status/{emailId}: + get: + description: Get the patron details by email ID + queryParameters: + apikey: + description: "API Key" + type: string + emailId: + description: The email ID of the patron. + type: string + required: true + responses: + 200: + description: patron information retrieved successfully + body: + application/json: + type: user + example: !include examples/user.json + 400: + description: Validation error + body: + application/json: + type: user_error_400 + example: !include examples/user_error.json + 401: + description: Not authorized to perform requested action + body: + text/plain: + example: unable to get account -- unauthorized + 403: + description: Access Denied + body: + text/plain: + example: Access Denied + 404: + description: Validation error + body: + application/json: + type: user_error_404 + example: !include examples/user_error.json + 500: + description: Internal server error, e.g. due to misconfiguration + body: + text/plain: + example: internal server error, contact administrator diff --git a/ramls/examples/user_error.json b/ramls/examples/user_error.json index bb7a6cb..3cb146d 100644 --- a/ramls/examples/user_error.json +++ b/ramls/examples/user_error.json @@ -1,4 +1,4 @@ { - "code": 404, - "errorMessage": "USER_ACCOUNT_INACTIVE" + "code": "USER_ACCOUNT_INACTIVE", + "errorMessage": "User account is not active" } diff --git a/ramls/schemas/user_error_400.schema b/ramls/schemas/user_error_400.schema index bc4bce5..55f2f61 100644 --- a/ramls/schemas/user_error_400.schema +++ b/ramls/schemas/user_error_400.schema @@ -1,21 +1,23 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "id": "external_patron_error.schema", - "description": "An external_patron user error", + "id": "user_error.schema", + "description": "user errors", "type": "object", "properties": { "code": { - "type": "integer", - "description": "Error code" + "type": "string", + "description": "Error code", + "examples": [ + "MULTIPLE_USER_WITH_EMAIL", + "EMAIL_NOT_PROVIDED" + ] }, "errorMessage": { "type": "string", - "description": "Error message text", + "description": "Error code description", "examples": [ - { - "value": "MULTIPLE_USER_EXISTS", - "description": "Multiple users found with the same email" - } + "Multiple users found with the same email", + "emailId is missing in the request" ] } }, diff --git a/ramls/schemas/user_error_404.schema b/ramls/schemas/user_error_404.schema index d5fcd28..31d65bb 100644 --- a/ramls/schemas/user_error_404.schema +++ b/ramls/schemas/user_error_404.schema @@ -1,25 +1,23 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "id": "external_patron_error.schema", - "description": "An external_patron user error", + "id": "user_error.schema", + "description": "user errors", "type": "object", "properties": { "code": { - "type": "integer", - "description": "Error code" + "type": "string", + "description": "Error code", + "examples": [ + "USER_ACCOUNT_INACTIVE", + "USER_NOT_FOUND" + ] }, "errorMessage": { "type": "string", - "description": "Error message text", + "description": "Error code description", "examples": [ - { - "value": "USER_ACCOUNT_INACTIVE", - "description": "User is not active" - }, - { - "value": "USER_NOT_FOUND", - "description": "User does not exist" - } + "User account is not active", + "User does not exist" ] } }, diff --git a/ramls/staging_user.json b/ramls/staging_user.json index 98bcc85..330f13d 100644 --- a/ramls/staging_user.json +++ b/ramls/staging_user.json @@ -5,7 +5,7 @@ "type": "object", "properties": { "isEmailVerified": { - "description": "A boolean flag that indicates whether the patron has completed email verification. If this value is not provided when creating a new record, it will default to false. However, for Kiosk user registrations, this value should be sent true.", + "description": "A boolean flag that indicates whether the patron has completed email verification. If this value is not provided when creating a new record, it will default to false. However, for Kiosk user registrations, this value should be sent false.", "type": "boolean" }, "status": { diff --git a/src/main/java/org/folio/edge/patron/MainVerticle.java b/src/main/java/org/folio/edge/patron/MainVerticle.java index 8219ef6..73ff66b 100644 --- a/src/main/java/org/folio/edge/patron/MainVerticle.java +++ b/src/main/java/org/folio/edge/patron/MainVerticle.java @@ -60,9 +60,6 @@ public Router defineRoutes() { router.route(HttpMethod.GET, "/patron/account/:patronId/external-patrons") .handler(patronHandler::handleGetExtPatronsAccounts); - router.route(HttpMethod.GET, "/patron/account/:patronId/by-email/:emailId") - .handler(patronHandler::handleGetExtPatronAccountByEmail); - router.route(HttpMethod.PUT, "/patron/account/:patronId/by-email/:emailId") .handler(patronHandler::handlePutExtPatronAccountByEmail); @@ -84,6 +81,9 @@ public Router defineRoutes() { router.route(HttpMethod.POST, "/patron/account/:patronId/hold/:holdId/cancel") .handler(patronHandler::handleCancelHold); + router.route(HttpMethod.GET, "/patron/registration-status") + .handler(patronHandler::handleGetPatronRegistrationStatus); + return router; } } diff --git a/src/main/java/org/folio/edge/patron/PatronHandler.java b/src/main/java/org/folio/edge/patron/PatronHandler.java index d4c0830..6efbeae 100644 --- a/src/main/java/org/folio/edge/patron/PatronHandler.java +++ b/src/main/java/org/folio/edge/patron/PatronHandler.java @@ -22,6 +22,7 @@ import static org.folio.edge.patron.Constants.PARAM_SORT_BY; import static org.folio.edge.patron.model.HoldCancellationValidator.validateCancelHoldRequest; +import com.amazonaws.util.StringUtils; import com.fasterxml.jackson.core.JsonProcessingException; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpHeaders; @@ -33,6 +34,7 @@ import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -42,6 +44,7 @@ import org.apache.logging.log4j.Logger; import org.folio.edge.core.Handler; import org.folio.edge.core.security.SecureStore; +import org.folio.edge.core.utils.Mappers; import org.folio.edge.core.utils.OkapiClient; import org.folio.edge.core.utils.OkapiClientFactory; import org.folio.edge.patron.model.error.Error; @@ -139,16 +142,6 @@ public void handleRenew(RoutingContext ctx) { } - public void handleGetExtPatronAccountByEmail(RoutingContext ctx) { - handleCommon(ctx, - new String[] { PARAM_PATRON_ID, PARAM_EMAIL_ID }, - new String[] {}, - (client, params) -> ((PatronOkapiClient) client).getExtPatronAccountByEmail( - params.get(PARAM_EMAIL_ID), - resp -> handleProxyResponse(ctx, resp), - t -> handleProxyException(ctx, t))); - } - public void handlePutExtPatronAccountByEmail(RoutingContext ctx) { if (ctx.body().asJsonObject() == null) { badRequest(ctx, MSG_EXTERNAL_NOBODY); @@ -260,6 +253,26 @@ public void handleGetAllowedServicePoints(RoutingContext ctx) { t -> handleProxyException(ctx, t))); } + public void handleGetPatronRegistrationStatus(RoutingContext ctx) { + logger.debug("handleGetPatronRegistrationStatus:: Fetching patron registration"); + String emailId = ctx.request().getParam(PARAM_EMAIL_ID); + if(StringUtils.isNullOrEmpty(emailId)) { + logger.warn("handleGetPatronRegistrationStatus:: Missing or empty emailId"); + ctx.response() + .setStatusCode(400) + .putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .end(getErrorMsg("EMAIL_NOT_PROVIDED", "emailId is missing in the request")); + return; + } + super.handleCommon(ctx, new String[]{}, new String[]{}, (client, params) -> { + String alternateTenantId = ctx.request().getParam("alternateTenantId", client.tenant); + final PatronOkapiClient patronClient = new PatronOkapiClient(client, alternateTenantId); + patronClient.getPatronRegistrationStatus(emailId, + resp -> handleRegistrationStatusResponse(ctx, resp), + t -> handleProxyException(ctx, t)); + }); + } + @Override protected void invalidApiKey(RoutingContext ctx, String msg) { accessDenied(ctx, msg); @@ -332,6 +345,32 @@ protected void handleProxyResponse(RoutingContext ctx, HttpResponse resp } } + protected void handleRegistrationStatusResponse(RoutingContext ctx, HttpResponse resp) { + HttpServerResponse serverResponse = ctx.response(); + + int statusCode = resp.statusCode(); + serverResponse.setStatusCode(statusCode); + + String respBody = resp.bodyAsString(); + if (logger.isDebugEnabled() ) { + logger.debug("handleRegistrationStatusResponse:: response {} ", respBody); + } + + String contentType = resp.getHeader(HttpHeaders.CONTENT_TYPE.toString()); + + if (resp.statusCode() < 400 && Objects.nonNull(respBody)){ + setContentType(serverResponse, contentType); + serverResponse.end(respBody); //not an error case, pass on the response body as received + } + else { + String errorMsg = (statusCode == 404 || statusCode == 400) + ? getFormattedErrorMsg(statusCode, respBody) + : getStructuredErrorMessage(statusCode, respBody); + setContentType(serverResponse, APPLICATION_JSON); + serverResponse.end(errorMsg); + } + } + @Override protected void handleProxyException(RoutingContext ctx, Throwable t) { logger.error("Exception retrieving data from mod-patron:", t); @@ -435,6 +474,33 @@ private String get422ErrorMsg(int statusCode, String respBody){ return errorMessage; } + private String getFormattedErrorMsg(int statusCode, String respBody) { + logger.debug("getFormattedErrorMsg:: respBody {}", respBody); + String errorMessage = ""; + try { + var errors = Json.decodeValue(respBody, Errors.class).getErrors(); + if (errors != null && !errors.isEmpty()) { + var error = errors.get(0); + return getErrorMsg(error.getCode(), error.getMessage()); + } + } catch (Exception ex) { + logger.warn(ex.getMessage()); + errorMessage = getStructuredErrorMessage(statusCode, respBody); + } + return errorMessage; + } + + private String getErrorMsg(String code, String errorMessage) { + Map errorMap = new HashMap<>(); + errorMap.put("errorMessage", errorMessage); + errorMap.put("code", code); + try { + return Mappers.jsonMapper.writeValueAsString(errorMap); + } catch (JsonProcessingException e) { + return getStructuredErrorMessage(500, "A problem encountered when extracting error message"); + } + } + private String getErrorMessage(int statusCode, String respBody){ if (statusCode == 422) diff --git a/src/main/java/org/folio/edge/patron/utils/PatronOkapiClient.java b/src/main/java/org/folio/edge/patron/utils/PatronOkapiClient.java index 3472e2a..10164be 100644 --- a/src/main/java/org/folio/edge/patron/utils/PatronOkapiClient.java +++ b/src/main/java/org/folio/edge/patron/utils/PatronOkapiClient.java @@ -95,17 +95,6 @@ public void getAccount(String patronId, boolean includeLoans, boolean includeCha exceptionHandler); } - public void getExtPatronAccountByEmail(String email, Handler> responseHandler, - Handler exceptionHandler) { - String url = String.format("%s/patron/account/by-email/%s", okapiURL, email); - get( - url, - tenant, - null, - responseHandler, - exceptionHandler); - } - public void getExtPatronAccounts(boolean expired, Handler> responseHandler, Handler exceptionHandler) { String url = String.format("%s/patron/account?expired=%s", okapiURL, expired); @@ -219,6 +208,13 @@ public void placeInstanceHold(String patronId, String instanceId, String request exceptionHandler); } + public void getPatronRegistrationStatus(String emailId, + Handler> responseHandler, Handler exceptionHandler) { + + get(String.format("%s/patron/registration-status/%s", okapiURL, + emailId), tenant, null, responseHandler, exceptionHandler); + } + private Hold createCancellationHoldRequest(JsonObject cancellationRequest, JsonObject baseRequest, String patronId) { return Hold.builder() .cancellationReasonId(cancellationRequest.getString(FIELD_CANCELLATION_REASON_ID)) diff --git a/src/test/java/org/folio/edge/patron/MainVerticleTest.java b/src/test/java/org/folio/edge/patron/MainVerticleTest.java index aa94411..9ec84e4 100644 --- a/src/test/java/org/folio/edge/patron/MainVerticleTest.java +++ b/src/test/java/org/folio/edge/patron/MainVerticleTest.java @@ -273,14 +273,110 @@ public void testGetAccountPatronNotFound(TestContext context) throws Exception { } @Test - public void testGetAccountByEmail(TestContext context) { - logger.info("=== Test request for getting external_patron by email ==="); + public void testGetPatronRegistrationStatusWithoutEmail(TestContext context) { - RestAssured - .get(String.format("/patron/account/%s/by-email/%s?apikey=%s", extPatronId, "fgh@mail", apiKey)) + var response = RestAssured + .get(String.format("/patron/registration-status?apikey=%s", apiKey)) + .then() + .statusCode(400) + .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .extract() + .response(); + + var jsonResponse = new JsonObject(response.body().asString()); + assertEquals("EMAIL_NOT_PROVIDED", jsonResponse.getString("code")); + assertEquals("emailId is missing in the request", jsonResponse.getString("errorMessage")); + + response = RestAssured + .get(String.format("/patron/registration-status?emailId=%s&apikey=%s", "", apiKey)) + .then() + .statusCode(400) + .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .extract() + .response(); + + jsonResponse = new JsonObject(response.body().asString()); + assertEquals("EMAIL_NOT_PROVIDED", jsonResponse.getString("code")); + assertEquals("emailId is missing in the request", jsonResponse.getString("errorMessage")); + } + + @Test + public void testGetPatronRegistrationStatusWithActiveEmail(TestContext context) { + + final var response = RestAssured + .get(String.format("/patron/registration-status?emailId=%s&apikey=%s", "active@folio.com", apiKey)) .then() .statusCode(200) - .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON); + .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .extract() + .response(); + + var expected = new JsonObject(readMockFile( + "/user_active.json")); + var actual = new JsonObject(response.body().asString()); + assertEquals(expected, actual); + } + + @Test + public void testGetPatronRegistrationStatusWithInvalidEmail() { + + final var response = RestAssured + .get(String.format("/patron/registration-status?emailId=%s&apikey=%s", "usernotfound@folio.com", apiKey)) + .then() + .statusCode(404) + .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .extract() + .response(); + + var jsonResponse = new JsonObject(response.body().asString()); + assertEquals("USER_NOT_FOUND", jsonResponse.getString("code")); + assertEquals("User does not exist", jsonResponse.getString("errorMessage")); + } + + @Test + public void testGetPatronRegistrationStatusWithMultipleUserEmail() { + + final var response = RestAssured + .get(String.format("/patron/registration-status?emailId=%s&apikey=%s", "multipleuser@folio.com", apiKey)) + .then() + .statusCode(400) + .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .extract() + .response(); + + var jsonResponse = new JsonObject(response.body().asString()); + assertEquals("MULTIPLE_USER_WITH_EMAIL", jsonResponse.getString("code")); + assertEquals("Multiple users found with the same email", jsonResponse.getString("errorMessage")); + } + + @Test + public void testGetPatronRegistrationStatusWithInvalidScenarios() { + + // when we are getting 404, we converted it to Errors.class. But there are cases where we get text/plain errors. + // In that case, code will return the error as it is. + var response = RestAssured + .get(String.format("/patron/registration-status?emailId=%s&apikey=%s", "invalid@folio.com", apiKey)) + .then() + .statusCode(404) + .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .extract() + .response(); + + var jsonResponse = new JsonObject(response.body().asString()); + assertEquals("404", jsonResponse.getString("code")); + assertEquals("Resource not found", jsonResponse.getString("errorMessage")); + + response = RestAssured + .get(String.format("/patron/registration-status?emailId=%s&apikey=%s", "empty@folio.com", apiKey)) + .then() + .statusCode(500) + .header(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .extract() + .response(); + + jsonResponse = new JsonObject(response.body().asString()); + assertEquals("500", jsonResponse.getString("code")); + assertEquals("unable to retrieve user details", jsonResponse.getString("errorMessage")); } @Test diff --git a/src/test/java/org/folio/edge/patron/utils/PatronMockOkapi.java b/src/test/java/org/folio/edge/patron/utils/PatronMockOkapi.java index f6a660d..2eea6e5 100644 --- a/src/test/java/org/folio/edge/patron/utils/PatronMockOkapi.java +++ b/src/test/java/org/folio/edge/patron/utils/PatronMockOkapi.java @@ -5,6 +5,7 @@ import static org.folio.edge.core.Constants.DAY_IN_MILLIS; import static org.folio.edge.core.Constants.TEXT_PLAIN; import static org.folio.edge.core.Constants.X_OKAPI_TOKEN; +import static org.folio.edge.patron.Constants.PARAM_EMAIL_ID; import static org.folio.edge.patron.Constants.PARAM_HOLD_ID; import static org.folio.edge.patron.Constants.PARAM_INCLUDE_CHARGES; import static org.folio.edge.patron.Constants.PARAM_INCLUDE_HOLDS; @@ -166,6 +167,9 @@ public Router defineRoutes() { router.route(HttpMethod.GET, "/circulation/requests/:requestId") .handler(this::getRequestHandler); + router.route(HttpMethod.GET, "/patron/registration-status/:emailId") + .handler(this::getRegistrationStatusHandler); + return router; } @@ -255,6 +259,42 @@ public void getExtPatronAccountHandler(RoutingContext ctx) { } } + public void getRegistrationStatusHandler(RoutingContext ctx) { + String token = ctx.request().getHeader(X_OKAPI_TOKEN); + String emailId = ctx.request().getParam(PARAM_EMAIL_ID); + if (token == null || !token.equals(MOCK_TOKEN)) { + ctx.response() + .setStatusCode(403) + .putHeader(HttpHeaders.CONTENT_TYPE, TEXT_PLAIN) + .end("Access requires permission: patron.account.get"); + } else if(emailId.equals("active@folio.com")) { + ctx.response() + .setStatusCode(200) + .putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .end(readMockFile("/user_active.json")); + } else if(emailId.equals("multipleuser@folio.com")) { + ctx.response() + .setStatusCode(400) + .putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .end(readMockFile("/multiple_user_error.json")); + } else if(emailId.equals("usernotfound@folio.com")) { + ctx.response() + .setStatusCode(404) + .putHeader(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON) + .end(readMockFile("/user_not_found_error.json")); + } else if(emailId.equals("invalid@folio.com")) { + ctx.response() + .setStatusCode(404) + .putHeader(HttpHeaders.CONTENT_TYPE, TEXT_PLAIN) + .end("Resource not found"); + } else { + ctx.response() + .setStatusCode(500) + .putHeader(HttpHeaders.CONTENT_TYPE, TEXT_PLAIN) + .end("unable to retrieve user details"); + } + } + public void putExtPatronAccountHandler(RoutingContext ctx) { String token = ctx.request().getHeader(X_OKAPI_TOKEN); if (token == null || !token.equals(MOCK_TOKEN)) { @@ -667,7 +707,6 @@ public static Patron getPatron() { .preferredEmailCommunication(new ArrayList<>()) .build(); } - public static Charge getCharge(String itemId) { return Charge.builder() .item(getItem(itemId_overdue)) diff --git a/src/test/resources/multiple_user_error.json b/src/test/resources/multiple_user_error.json new file mode 100644 index 0000000..4f1468b --- /dev/null +++ b/src/test/resources/multiple_user_error.json @@ -0,0 +1,9 @@ +{ + "errors": [ + { + "message": "Multiple users found with the same email", + "code": "MULTIPLE_USER_WITH_EMAIL", + "parameters": [] + } + ] +} diff --git a/src/test/resources/user_active.json b/src/test/resources/user_active.json new file mode 100644 index 0000000..cc3e1ae --- /dev/null +++ b/src/test/resources/user_active.json @@ -0,0 +1,46 @@ +{ + "id": "cacc29d8-cade-4312-a5f2-4eeac55d8697", + "externalSystemId": "active@folio.com", + "active": true, + "type": "patron", + "patronGroup": "63f8065f-df84-4e76-a36b-3ba32dbdc9e5", + "departments": [], + "proxyFor": [], + "personal": { + "lastName": "active", + "firstName": "folio", + "middleName": "", + "preferredFirstName": "new", + "email": "active@folio.com", + "phone": "555-123456", + "mobilePhone": "555-5678", + "addresses": [ + { + "id": "ec8c23d5-c301-4bba-8ade-39cc409e5d7e", + "countryId": "US", + "addressLine1": "123 Main St", + "addressLine2": "Apt 8", + "city": "Metropolis", + "region": "NY", + "postalCode": "12345", + "addressTypeId": "93d3d88d-499b-45d0-9bc7-ac73c3a19880", + "primaryAddress": true + } + ], + "preferredContactTypeId": "002" + }, + "enrollmentDate": "2024-08-29T13:29:39.248+00:00", + "expirationDate": "2026-08-29T00:00:00.000+00:00", + "createdDate": "2024-08-29T13:29:39.256+00:00", + "updatedDate": "2024-08-29T13:29:39.256+00:00", + "metadata": { + "createdDate": "2024-08-29T13:29:39.250+00:00", + "createdByUserId": "21457ab5-4635-4e56-906a-908f05e9233b", + "updatedDate": "2024-08-29T13:29:39.250+00:00", + "updatedByUserId": "21457ab5-4635-4e56-906a-908f05e9233b" + }, + "preferredEmailCommunication": [ + "Support", + "Programs" + ] +} diff --git a/src/test/resources/user_not_found_error.json b/src/test/resources/user_not_found_error.json new file mode 100644 index 0000000..22f4a31 --- /dev/null +++ b/src/test/resources/user_not_found_error.json @@ -0,0 +1,9 @@ +{ + "errors": [ + { + "message": "User does not exist", + "code": "USER_NOT_FOUND", + "parameters": [] + } + ] +}