From aeb8a433935f0dcdc8ad57e034064576c55eba7d Mon Sep 17 00:00:00 2001 From: VadymZakletskyi Date: Tue, 3 Mar 2026 11:53:19 +0200 Subject: [PATCH 1/3] CIRC-2550 Update event publishing for loan renewals and add tests for logging --- .../circulation/services/EventPublisher.java | 18 +++- .../services/EventPublisherTest.java | 100 ++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/folio/circulation/services/EventPublisherTest.java diff --git a/src/main/java/org/folio/circulation/services/EventPublisher.java b/src/main/java/org/folio/circulation/services/EventPublisher.java index 034a5f996b..e55ae32707 100644 --- a/src/main/java/org/folio/circulation/services/EventPublisher.java +++ b/src/main/java/org/folio/circulation/services/EventPublisher.java @@ -201,7 +201,9 @@ public CompletableFuture> publishDueDateChangedEvent( renewalContext.getLoan() != null ? renewalContext.getLoan().getId() : "null"); var loan = renewalContext.getLoan(); - publishDueDateChangedEvent(loan, loan.getUser(), true); + publishDueDateChangedEvent(loan, loan.getUser(), false); + runAsync(() -> publishRenewedEvent(loan.copy().withUser(loan.getUser()), + renewalContext.getLoggedInUserId())); return completedFuture(succeeded(renewalContext)); } @@ -329,6 +331,20 @@ public CompletableFuture> publishRenewedEvent(Loan loan) { .thenCompose(loanLogContext -> loanLogContext.after(ctx -> publishLogRecord(ctx, LOAN))); } + 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))); + } + public CompletableFuture> publishNoticeLogEvent(NoticeLogContext noticeLogContext, Result previousStepResult, Throwable throwable) { 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))); + } +} From b1d498e74be9a7cd4fe2d44e9c8bf3020be072da Mon Sep 17 00:00:00 2001 From: VadymZakletskyi Date: Tue, 3 Mar 2026 13:51:41 +0200 Subject: [PATCH 2/3] CIRC-2550 Refactor event publishing for due date changes to support renewal context --- .../circulation/services/EventPublisher.java | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/folio/circulation/services/EventPublisher.java b/src/main/java/org/folio/circulation/services/EventPublisher.java index e55ae32707..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,9 +201,7 @@ public CompletableFuture> publishDueDateChangedEvent( renewalContext.getLoan() != null ? renewalContext.getLoan().getId() : "null"); var loan = renewalContext.getLoan(); - publishDueDateChangedEvent(loan, loan.getUser(), false); - runAsync(() -> publishRenewedEvent(loan.copy().withUser(loan.getUser()), - renewalContext.getLoggedInUserId())); + publishDueDateChangedEvent(loan, loan.getUser(), renewalContext); return completedFuture(succeeded(renewalContext)); } @@ -319,18 +317,6 @@ 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); - return getTenantTimeZone() - .thenApply(zoneResult -> zoneResult.map(zoneId -> { - var logDescription = getLoanDueDateChangeLog(loan, zoneId); - return LoanLogContext.from(loan) - .withDescription(logDescription) - .asJson(); - })) - .thenCompose(loanLogContext -> loanLogContext.after(ctx -> publishLogRecord(ctx, LOAN))); - } - private CompletableFuture> publishRenewedEvent(Loan loan, String updatedByUserId) { logger.info("publishRenewedEvent:: parameters loanId: {}, updatedByUserId: {}", loan::getId, () -> updatedByUserId); @@ -483,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()); @@ -495,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()) From 278360a44d0b7f2ae9ba127cf883fc8e65b35ee7 Mon Sep 17 00:00:00 2001 From: VadymZakletskyi Date: Tue, 3 Mar 2026 15:13:21 +0200 Subject: [PATCH 3/3] CIRC-2550 Update readme --- NEWS.md | 1 + 1 file changed, 1 insertion(+) 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)