Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Use GitHub Workflows for Maven ([SIP2-307](https://folio-org.atlassian.net/browse/SIP2-307))
* Fix misleading log messages when token refresh fails and recovery login is used([SIP2-316](https://folio-org.atlassian.net/browse/SIP2-316))
* Fix locale agnostic number formatting ([SIP2-300](https://folio-org.atlassian.net/browse/SIP2-300))
* Add automated patron blocks support to patron status/information response ([SIP2-313](https://folio-org.atlassian.net/browse/SIP2-313))

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,63 @@ public Future<JsonObject> getFeeFinesByIds(List<String> ids, SessionData session
.map(IResource::getResource);
}

/**
* Get a patron's automated patron blocks.
*
* @param userId the user's ID
* @param sessionData session data
* @return the automated patron blocks list in raw JSON or {@code null} if there was an error
*/
public Future<JsonObject> getAutomatedBlocksByUserId(String userId, SessionData sessionData) {
Objects.requireNonNull(userId, "userId cannot be null");
Objects.requireNonNull(sessionData, "sessionData cannot be null");

final Map<String, String> headers = new HashMap<>();
headers.put(HEADER_ACCEPT, MIMETYPE_JSON);

final GetAutomatedBlocksByUserIdRequestData requestData =
new GetAutomatedBlocksByUserIdRequestData(userId, headers, sessionData);

return resourceProvider.retrieveResource(requestData)
.onFailure(t -> logAutomatedPatronBlocksFetchError(userId, sessionData, t))
.otherwise(() -> null)
.map(IResource::getResource);
}

private void logAutomatedPatronBlocksFetchError(String userId, SessionData sessionData,
Throwable t) {
log.warn(sessionData,
"Failed to retrieve automated patron blocks for user {}: {}", userId, t.getMessage());
}

protected static class GetAutomatedBlocksByUserIdRequestData implements IRequestData {
private final String userId;
private final Map<String, String> headers;
private final SessionData sessionData;

protected GetAutomatedBlocksByUserIdRequestData(String userId, Map<String, String> headers,
SessionData sessionData) {
this.userId = userId;
this.headers = Map.copyOf(headers);
this.sessionData = sessionData;
}

@Override
public String getPath() {
return "/automated-patron-blocks/" + userId;
}

@Override
public Map<String, String> getHeaders() {
return headers;
}

@Override
public SessionData getSessionData() {
return sessionData;
}
}

protected static class GetManualBlocksByUserIdRequestData implements IRequestData {
private final String userId;
private final Map<String, String> headers;
Expand Down
119 changes: 89 additions & 30 deletions src/main/java/org/folio/edge/sip2/repositories/PatronRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -263,10 +263,11 @@ private Future<PatronInformationResponse> validPatron(ExtendedUser extendedUser,
addPersonalData(personal, patronInformation.getPatronIdentifier(), builder);
final Integer startItem = patronInformation.getStartItem();
final Integer endItem = patronInformation.getEndItem();
// Get manual blocks data to build patron status
final Future<PatronInformationResponseBuilder> manualBlocksFuture = feeFinesRepository
.getManualBlocksByUserId(userId, sessionData)
.map(blocks -> buildPatronStatus(blocks, builder));
// Get manual and automated blocks data to build patron status
final Future<PatronInformationResponseBuilder> patronStatusFuture = Future.all(
feeFinesRepository.getManualBlocksByUserId(userId, sessionData),
feeFinesRepository.getAutomatedBlocksByUserId(userId, sessionData))
.map(cf -> buildPatronStatus(cf.resultAt(0), cf.resultAt(1), builder));
// Add fine count
final Future<PatronInformationResponseBuilder> accountFuture = feeFinesRepository
.getAccountDataByUserId(userId, sessionData)
Expand Down Expand Up @@ -306,7 +307,7 @@ private Future<PatronInformationResponse> validPatron(ExtendedUser extendedUser,
getRecalls(userId, sessionData).compose(recalls -> addRecalls(recalls, startItem, endItem,
patronInformation.getSummary() == RECALL_ITEMS, builder));
// When all operations complete, build and return the final PatronInformationResponse
return Future.all(manualBlocksFuture, accountFuture, holdsFuture,
return Future.all(patronStatusFuture, accountFuture, holdsFuture,
overdueFuture, recallsFuture, loansFuture)
.map(result -> {
log.info(sessionData, "validPatron language:{} institutionId:{}",
Expand Down Expand Up @@ -349,18 +350,24 @@ private Future<PatronStatusResponse> validPatron(ExtendedUser extendedUser,
.getFeeAmountByUserId(userId, sessionData)
.map(accounts -> totalAmount(sessionData, accounts, builder));

final Future<PatronStatusResponseBuilder> manualBlocksFuture = feeFinesRepository
.getManualBlocksByUserId(userId, sessionData)
.map(blocks -> {
builder.patronStatus(extractPatronStatusFromBlocks(blocks));
final List<String> messages = extractBlockMessages(blocks);
final Future<PatronStatusResponseBuilder> blocksFuture = Future.all(
feeFinesRepository.getManualBlocksByUserId(userId, sessionData),
feeFinesRepository.getAutomatedBlocksByUserId(userId, sessionData))
.map(cf -> {
var manualBlocks = (JsonObject) cf.resultAt(0);
var automatedBlocks = (JsonObject) cf.resultAt(1);
var patronStatusFlags = extractPatronStatusFromBlocks(manualBlocks);
patronStatusFlags.addAll(extractPatronStatusFromAutomatedBlocks(automatedBlocks));
builder.patronStatus(patronStatusFlags);
var messages = new ArrayList<>(extractBlockMessages(manualBlocks));
messages.addAll(extractAutomatedBlockMessages(automatedBlocks));
if (!messages.isEmpty()) {
builder.screenMessage(messages);
}
return builder;
});

return Future.all(getFeeAmountFuture, manualBlocksFuture).map(cf -> builder
return Future.all(getFeeAmountFuture, blocksFuture).map(cf -> builder
.language(patronStatus.getLanguage())
.transactionDate(OffsetDateTime.now(clock))
.institutionId(patronStatus.getInstitutionId())
Expand Down Expand Up @@ -474,41 +481,93 @@ private Future<PatronStatusResponse> invalidPatron(
}


private PatronInformationResponseBuilder buildPatronStatus(JsonObject blocks,
private PatronInformationResponseBuilder buildPatronStatus(JsonObject manualBlocks,
JsonObject automatedBlocks,
PatronInformationResponseBuilder builder) {
final EnumSet<PatronStatus> patronStatus = extractPatronStatusFromBlocks(blocks);
var patronStatus = extractPatronStatusFromBlocks(manualBlocks);
patronStatus.addAll(extractPatronStatusFromAutomatedBlocks(automatedBlocks));

if (!patronStatus.isEmpty()) {
builder.screenMessage(extractBlockMessages(blocks));
var messages = new ArrayList<>(extractBlockMessages(manualBlocks));
messages.addAll(extractAutomatedBlockMessages(automatedBlocks));
builder.screenMessage(messages);
}

return builder.patronStatus(patronStatus);
}

private static EnumSet<PatronStatus> extractPatronStatusFromAutomatedBlocks(JsonObject blocks) {
final var patronStatus = EnumSet.noneOf(PatronStatus.class);

if (blocks != null) {
blocks.getJsonArray("automatedPatronBlocks", new JsonArray()).stream()
.map(o -> (JsonObject) o)
.map(jo -> toBlockStatusFlags(
jo.getBoolean("blockBorrowing", FALSE),
jo.getBoolean("blockRenewals", FALSE),
jo.getBoolean("blockRequests", FALSE)))
.forEach(patronStatus::addAll);
}

return patronStatus;
}

protected static List<String> extractAutomatedBlockMessages(JsonObject blocks) {
if (blocks == null) {
return Collections.emptyList();
}

return blocks.getJsonArray("automatedPatronBlocks", new JsonArray()).stream()
.map(o -> (JsonObject) o)
.filter(PatronRepository::hasAnyAutomatedBlock)
.map(PatronRepository::resolveAutomatedBlockMessage)
.toList();
}

private static boolean hasAnyAutomatedBlock(JsonObject jo) {
return jo.getBoolean("blockBorrowing", FALSE)
|| jo.getBoolean("blockRenewals", FALSE)
|| jo.getBoolean("blockRequests", FALSE);
}

private static String resolveAutomatedBlockMessage(JsonObject jo) {
var message = jo.getString("message");
return StringUtils.isNotBlank(message) ? message : MESSAGE_BLOCKED_PATRON;
}

private static EnumSet<PatronStatus> extractPatronStatusFromBlocks(JsonObject blocks) {
final EnumSet<PatronStatus> patronStatus = EnumSet.noneOf(PatronStatus.class);
final var patronStatus = EnumSet.noneOf(PatronStatus.class);

if (blocks != null && blocks.getInteger(FIELD_TOTAL_RECORDS, 0) > 0) {
blocks.getJsonArray("manualblocks", new JsonArray()).stream()
.map(o -> (JsonObject) o)
.forEach(jo -> {
if (jo.getBoolean("borrowing", FALSE)) {
patronStatus.addAll(EnumSet.allOf(PatronStatus.class));
} else {
if (jo.getBoolean("renewals", FALSE)) {
patronStatus.add(RENEWAL_PRIVILEGES_DENIED);
}
if (jo.getBoolean(FIELD_REQUESTS, FALSE)) {
patronStatus.add(HOLD_PRIVILEGES_DENIED);
patronStatus.add(RECALL_PRIVILEGES_DENIED);
}
}
});
.map(jo -> toBlockStatusFlags(
jo.getBoolean("borrowing", FALSE),
jo.getBoolean("renewals", FALSE),
jo.getBoolean(FIELD_REQUESTS, FALSE)))
.forEach(patronStatus::addAll);
}

return patronStatus;
}

private static EnumSet<PatronStatus> toBlockStatusFlags(
boolean borrowing, boolean renewals, boolean requests) {
var flags = EnumSet.noneOf(PatronStatus.class);
if (borrowing) {
flags.addAll(EnumSet.allOf(PatronStatus.class));
} else {
if (renewals) {
flags.add(RENEWAL_PRIVILEGES_DENIED);
}
if (requests) {
flags.add(HOLD_PRIVILEGES_DENIED);
flags.add(RECALL_PRIVILEGES_DENIED);
}
}
return flags;
}

protected static List<String> extractBlockMessages(JsonObject blocks) {
if (blocks == null || blocks.getInteger(FIELD_TOTAL_RECORDS, 0) == 0) {
return Collections.emptyList();
Expand All @@ -520,11 +579,11 @@ protected static List<String> extractBlockMessages(JsonObject blocks) {
|| jo.getBoolean("renewals", FALSE)
|| jo.getBoolean(FIELD_REQUESTS, FALSE))
.map(jo -> {
final String patronMessage = jo.getString("patronMessage");
var patronMessage = jo.getString("patronMessage");
if (StringUtils.isNotBlank(patronMessage)) {
return patronMessage;
}
final String desc = jo.getString("desc");
var desc = jo.getString("desc");
if (StringUtils.isNotBlank(desc)) {
return desc;
}
Expand Down
42 changes: 42 additions & 0 deletions src/test/java/org/folio/edge/sip2/api/PatronInformationIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class PatronInformationIT extends AbstractErrorDetectionEnabledTest {
"/wiremock/stubs/mod-fee-fines/200-get-accounts.json",
"/wiremock/stubs/mod-fee-fines/200-get-manualblocks.json",
"/wiremock/stubs/mod-fee-fines/200-get-feefines-empty.json",
"/wiremock/stubs/mod-fee-fines/200-get-automated-patron-blocks.json",
})
void getPatronInformation_positive_holdSummaryType() throws Throwable {
var currentTs = OffsetDateTime.now().toInstant();
Expand Down Expand Up @@ -78,6 +79,7 @@ void getPatronInformation_positive_holdSummaryType() throws Throwable {
"/wiremock/stubs/mod-fee-fines/200-get-accounts-empty.json",
"/wiremock/stubs/mod-fee-fines/200-get-manualblocks.json",
"/wiremock/stubs/mod-fee-fines/500-get-feefines-invalid-query.json",
"/wiremock/stubs/mod-fee-fines/200-get-automated-patron-blocks.json",
})
void getPatronInformation_positive_holdSummaryTypeAndEmptyRequests() throws Throwable {
executeInSession(
Expand Down Expand Up @@ -116,6 +118,7 @@ void getPatronInformation_positive_holdSummaryTypeAndEmptyRequests() throws Thro
"/wiremock/stubs/mod-fee-fines/200-get-accounts.json",
"/wiremock/stubs/mod-fee-fines/200-get-manualblocks.json",
"/wiremock/stubs/mod-fee-fines/200-get-feefines-empty.json",
"/wiremock/stubs/mod-fee-fines/200-get-automated-patron-blocks.json",
})
void getPatronInformationWithPasswordVerificationRequired_invalidPassword() throws Throwable {
executeInSession(
Expand Down Expand Up @@ -155,6 +158,7 @@ void getPatronInformationWithPasswordVerificationRequired_invalidPassword() thro
"/wiremock/stubs/mod-fee-fines/200-get-accounts.json",
"/wiremock/stubs/mod-fee-fines/200-get-manualblocks.json",
"/wiremock/stubs/mod-fee-fines/200-get-feefines-empty.json",
"/wiremock/stubs/mod-fee-fines/200-get-automated-patron-blocks.json",
})
void getPatronInformationWithPasswordVerificationRequired_validPassword() throws Throwable {
executeInSession(
Expand All @@ -179,6 +183,44 @@ void getPatronInformationWithPasswordVerificationRequired_validPassword() throws
));
}

@Test
@WiremockStubs({
"/wiremock/stubs/mod-settings/200-get-locale.json",
"/wiremock/stubs/mod-settings/200-get-settings.json",
"/wiremock/stubs/mod-login/201-post-acs-login.json",
"/wiremock/stubs/mod-users/200-get-user-by-patron-identifier.json",
"/wiremock/stubs/mod-users-bl/200-get-user-by-id.json",
"/wiremock/stubs/mod-circulation/200-get-circulation-open-loans.json",
"/wiremock/stubs/mod-circulation/200-get-circulation-open-loans-by-due-date.json",
"/wiremock/stubs/mod-circulation/200-get-circulation-requests-hold.json",
"/wiremock/stubs/mod-circulation/200-get-circulation-requests-recall.json",
"/wiremock/stubs/mod-fee-fines/200-get-accounts.json",
"/wiremock/stubs/mod-fee-fines/200-get-manualblocks.json",
"/wiremock/stubs/mod-fee-fines/200-get-feefines-empty.json",
"/wiremock/stubs/mod-fee-fines/200-get-automated-patron-blocks-with-borrowing-block.json",
})
void getPatronInformation_withAutomatedBorrowingBlock_allStatusFlagsSetAndMessageReturned()
throws Throwable {
executeInSession(
successLoginExchange(),
sip2Exchange(
Sip2Commands.patronInformation(PATRON_BARCODE, HOLD_ITEMS),
sip2Result -> {
assertSuccessfulExchange(sip2Result);

var respMsg = sip2Result.getResponseMessage();
assertThat(respMsg).startsWith("64");

var patronInfo = parseResponse(respMsg);
assertThat(patronInfo.getValidPatron()).isTrue();
assertThat(patronInfo.getPatronStatus())
.isEqualTo(EnumSet.allOf(PatronStatus.class));
assertThat(patronInfo.getScreenMessage())
.isEqualTo(List.of("Patron has too many items checked out"));
}
));
}

private static PatronInformationResponse parseResponse(String respMsg) {
return new PatronInformationResponseParser(delimiter, TIMEZONE).parse(respMsg);
}
Expand Down
35 changes: 35 additions & 0 deletions src/test/java/org/folio/edge/sip2/api/PatronStatusIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class PatronStatusIT extends AbstractErrorDetectionEnabledTest {
"/wiremock/stubs/mod-users-bl/200-get-user-by-id.json",
"/wiremock/stubs/mod-fee-fines/200-get-accounts-open-status.json",
"/wiremock/stubs/mod-fee-fines/200-get-manualblocks.json",
"/wiremock/stubs/mod-fee-fines/200-get-automated-patron-blocks.json",
})
void getPatronStatus_noManualBlocks_patronStatusFlagsAreEmpty() throws Throwable {
executeInSession(
Expand Down Expand Up @@ -57,6 +58,7 @@ void getPatronStatus_noManualBlocks_patronStatusFlagsAreEmpty() throws Throwable
"/wiremock/stubs/mod-users-bl/200-get-user-by-id.json",
"/wiremock/stubs/mod-fee-fines/200-get-accounts-open-status.json",
"/wiremock/stubs/mod-fee-fines/200-get-manualblocks-with-borrowing-block.json",
"/wiremock/stubs/mod-fee-fines/200-get-automated-patron-blocks.json",
})
void getPatronStatus_withManualBorrowingBlock_allStatusFlagsSetAndMessageReturned()
throws Throwable {
Expand All @@ -81,6 +83,39 @@ void getPatronStatus_withManualBorrowingBlock_allStatusFlagsSetAndMessageReturne
));
}

@Test
@WiremockStubs({
"/wiremock/stubs/mod-settings/200-get-locale.json",
"/wiremock/stubs/mod-settings/200-get-settings.json",
"/wiremock/stubs/mod-login/201-post-acs-login.json",
"/wiremock/stubs/mod-users/200-get-user-by-patron-identifier.json",
"/wiremock/stubs/mod-users-bl/200-get-user-by-id.json",
"/wiremock/stubs/mod-fee-fines/200-get-accounts-open-status.json",
"/wiremock/stubs/mod-fee-fines/200-get-manualblocks.json",
"/wiremock/stubs/mod-fee-fines/200-get-automated-patron-blocks-with-borrowing-block.json",
})
void getPatronStatus_withAutomatedBorrowingBlock_allStatusFlagsSetAndMessageReturned()
throws Throwable {
executeInSession(
successLoginExchange(),
sip2Exchange(
Sip2Commands.patronStatus(PATRON_BARCODE),
sip2Result -> {
assertSuccessfulExchange(sip2Result);

var respMsg = sip2Result.getResponseMessage();
assertThat(respMsg).startsWith("24");

var response = parseResponse(respMsg);
assertThat(response.getValidPatron()).isTrue();
assertThat(response.getPatronStatus())
.isEqualTo(EnumSet.allOf(PatronStatus.class));
assertThat(response.getScreenMessage())
.isEqualTo(List.of("Patron has too many items checked out"));
}
));
}

private PatronStatusResponse parseResponse(String respMsg) {
return new PatronStatusResponseParser(delimiter, TIMEZONE).parse(respMsg);
}
Expand Down
Loading
Loading