diff --git a/NEWS.md b/NEWS.md index 8c9b61b3e7..2cbdd82325 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,7 @@ * Switch DCB user check from lastName to the user type ([CIRC-2482](https://folio-org.atlassian.net/browse/CIRC-2482)) * Upgrade wiremock from 2.35.0 to 3.13.2 fixing CVE-2023-41329 ([CIRC-2543](https://folio-org.atlassian.net/browse/CIRC-2543)) * Fix request losing retrieval service point name upon check-in ([CIRC-2535](https://folio-org.atlassian.net/browse/CIRC-2535)) +* Fix circulation log showing incorrect source for loan renewals ([CIRC-2550](https://folio-org.atlassian.net/browse/CIRC-2550)) ## 24.4.0 2025-03-12 * Patron notices for the trigger “Item recalled” not sent if the item is not 1st in the title request queue (CIRC-2168) diff --git a/src/main/java/org/folio/circulation/services/EventPublisher.java b/src/main/java/org/folio/circulation/services/EventPublisher.java index 034a5f996b..cc49feff22 100644 --- a/src/main/java/org/folio/circulation/services/EventPublisher.java +++ b/src/main/java/org/folio/circulation/services/EventPublisher.java @@ -188,7 +188,7 @@ public CompletableFuture> publishDueDateChangedEve loanAndRelatedRecords.getLoan() != null ? loanAndRelatedRecords.getLoan().getId() : "null"); if (loanAndRelatedRecords.getLoan() != null) { Loan loan = loanAndRelatedRecords.getLoan(); - publishDueDateChangedEvent(loan, loan.getUser(), false); + publishDueDateChangedEvent(loan, loan.getUser(), null); } return completedFuture(succeeded(loanAndRelatedRecords)); @@ -201,7 +201,7 @@ public CompletableFuture> publishDueDateChangedEvent( renewalContext.getLoan() != null ? renewalContext.getLoan().getId() : "null"); var loan = renewalContext.getLoan(); - publishDueDateChangedEvent(loan, loan.getUser(), true); + publishDueDateChangedEvent(loan, loan.getUser(), renewalContext); return completedFuture(succeeded(renewalContext)); } @@ -317,13 +317,15 @@ public CompletableFuture> publishDueDateLogEvent(Loan loan) { .thenCompose(loanLogContext -> loanLogContext.after(ctx -> publishLogRecord(ctx, LOAN))); } - public CompletableFuture> publishRenewedEvent(Loan loan) { - logger.info("publishRenewedEvent:: parameters loanId: {}", loan::getId); + private CompletableFuture> publishRenewedEvent(Loan loan, String updatedByUserId) { + logger.info("publishRenewedEvent:: parameters loanId: {}, updatedByUserId: {}", + loan::getId, () -> updatedByUserId); return getTenantTimeZone() .thenApply(zoneResult -> zoneResult.map(zoneId -> { var logDescription = getLoanDueDateChangeLog(loan, zoneId); return LoanLogContext.from(loan) .withDescription(logDescription) + .withUpdatedByUserId(updatedByUserId) .asJson(); })) .thenCompose(loanLogContext -> loanLogContext.after(ctx -> publishLogRecord(ctx, LOAN))); @@ -467,10 +469,12 @@ private CompletableFuture> publishDueDateChangedEvent(Loan loan, Re if (records.getRecalledLoanPreviousDueDate() != null) { loan.setPreviousDueDate(records.getRecalledLoanPreviousDueDate()); } - return publishDueDateChangedEvent(loan, records.getRequest().getRequester(), false); + return publishDueDateChangedEvent(loan, records.getRequest().getRequester(), null); } - private CompletableFuture> publishDueDateChangedEvent(Loan loan, User user, boolean renewalContext) { + private CompletableFuture> publishDueDateChangedEvent(Loan loan, User user, + RenewalContext renewalContext) { + if (loan != null) { JsonObject payloadJsonObject = new JsonObject(); write(payloadJsonObject, USER_ID_FIELD, loan.getUserId()); @@ -479,8 +483,9 @@ private CompletableFuture> publishDueDateChangedEvent(Loan loan, Us write(payloadJsonObject, DUE_DATE_CHANGED_BY_RECALL_FIELD, loan.wasDueDateChangedByRecall()); runAsync(() -> publishDueDateLogEvent(loan)); - if (renewalContext) { - runAsync(() -> publishRenewedEvent(loan.copy().withUser(user))); + if (renewalContext != null) { + runAsync(() -> publishRenewedEvent(loan.copy().withUser(user), + renewalContext.getLoggedInUserId())); } return pubSubPublishingService.publishEvent(LOAN_DUE_DATE_CHANGED.name(), payloadJsonObject.encode()) diff --git a/src/test/java/org/folio/circulation/services/EventPublisherTest.java b/src/test/java/org/folio/circulation/services/EventPublisherTest.java new file mode 100644 index 0000000000..3538b086c4 --- /dev/null +++ b/src/test/java/org/folio/circulation/services/EventPublisherTest.java @@ -0,0 +1,100 @@ +package org.folio.circulation.services; + +import static java.util.concurrent.ForkJoinPool.commonPool; +import static org.folio.circulation.domain.EventType.LOG_RECORD; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.time.ZonedDateTime; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.folio.circulation.domain.Loan; +import org.folio.circulation.resources.context.RenewalContext; +import org.folio.circulation.support.Clients; +import org.folio.circulation.support.CollectionResourceClient; +import org.folio.circulation.support.GetManyRecordsClient; +import org.folio.circulation.support.ServerErrorFailure; +import org.folio.circulation.support.results.Result; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import io.vertx.core.json.JsonObject; + +@ExtendWith(MockitoExtension.class) +class EventPublisherTest { + + @Mock + private Clients clients; + @Mock + private PubSubPublishingService pubSubPublishingService; + @Mock + private CollectionResourceClient localeClient; + @Mock + private GetManyRecordsClient settingsStorageClient; + + private EventPublisher eventPublisher; + + @BeforeEach + void setUp() { + when(clients.pubSubPublishingService()).thenReturn(pubSubPublishingService); + when(clients.localeClient()).thenReturn(localeClient); + when(clients.settingsStorageClient()).thenReturn(settingsStorageClient); + when(pubSubPublishingService.publishEvent(anyString(), anyString())) + .thenReturn(CompletableFuture.completedFuture(true)); + when(localeClient.get()) + .thenReturn(CompletableFuture.completedFuture( + Result.failed(new ServerErrorFailure("locale not available")))); + + eventPublisher = new EventPublisher(clients); + } + + @Test + void renewalCirculationLogSourceShouldBeRenewalStaffNotCheckoutStaff() throws Exception { + String checkoutStaffId = UUID.randomUUID().toString(); + String renewalStaffId = UUID.randomUUID().toString(); + ZonedDateTime previousDueDate = ZonedDateTime.now().minusDays(7); + ZonedDateTime newDueDate = ZonedDateTime.now().plusDays(7); + + Loan loan = Loan.from(new JsonObject() + .put("id", UUID.randomUUID().toString()) + .put("userId", UUID.randomUUID().toString()) + .put("itemId", UUID.randomUUID().toString()) + .put("action", "renewed") + .put("dueDate", newDueDate.toString()) + .put("status", new JsonObject().put("name", "Open")) + .put("metadata", new JsonObject() + .put("updatedByUserId", checkoutStaffId))); + loan.setPreviousDueDate(previousDueDate); + + RenewalContext renewalContext = RenewalContext.create(loan, new JsonObject(), renewalStaffId); + eventPublisher.publishDueDateChangedEvent(renewalContext).get(); + commonPool().awaitQuiescence(5, TimeUnit.SECONDS); + + ArgumentCaptor payloadCaptor = ArgumentCaptor.forClass(String.class); + verify(pubSubPublishingService, atLeastOnce()) + .publishEvent(eq(LOG_RECORD.name()), payloadCaptor.capture()); + + JsonObject renewalLogPayload = payloadCaptor.getAllValues().stream() + .map(JsonObject::new) + .filter(json -> "Renewed".equals( + json.getJsonObject("payload", new JsonObject()).getString("action"))) + .findFirst() + .orElseThrow(() -> new AssertionError("No renewal LOG_RECORD event was published")); + String updatedByUserId = renewalLogPayload.getJsonObject("payload").getString("updatedByUserId"); + + assertThat(updatedByUserId, is(renewalStaffId)); + assertThat(updatedByUserId, is(not(checkoutStaffId))); + } +}