From 6e033029607367b643f9c082bcea2a2d64ad008a Mon Sep 17 00:00:00 2001 From: Kapil Verma <85541312+kapil-epam@users.noreply.github.com> Date: Fri, 6 Dec 2024 18:16:18 +0530 Subject: [PATCH 1/7] CIRCSTORE-539: added new request.item fields and db indexing into request schema (#498) * CIRCSTORE-539: added new request.item fields and db indexing * CIRCSTORE-539: fixed PR comments * CIRCSTORE-539: upgraded request-storage interface version from 6.1 to 6.2 * CIRCSTORE-539: fix sonar issue --- descriptors/ModuleDescriptor-template.json | 2 +- ramls/examples/request.json | 7 ++- ramls/request.json | 18 +++++++ .../templates/db_scripts/schema.json | 48 +++++++++++++++++++ .../org/folio/rest/api/RequestsApiTest.java | 13 ++++- .../support/builders/RequestItemSummary.java | 19 +++++++- .../builders/RequestRequestBuilder.java | 13 +++-- 7 files changed, 111 insertions(+), 9 deletions(-) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 4e775f55c..4ce7a918e 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -179,7 +179,7 @@ }, { "id": "request-storage", - "version": "6.1", + "version": "6.2", "handlers": [ { "methods": ["GET"], diff --git a/ramls/examples/request.json b/ramls/examples/request.json index efeca2d28..ce1d9f2d8 100644 --- a/ramls/examples/request.json +++ b/ramls/examples/request.json @@ -28,7 +28,12 @@ "holdingsRecordId": "e63273e7-48f5-4c43-ab4e-1751ecacaa21", "itemId": "195efae1-588f-47bd-a181-13a2eb437701", "item": { - "barcode": "760932543816" + "barcode": "760932543816", + "itemEffectiveLocationId" : "758258bc-ecc1-41b8-abca-f7b610822ffd", + "itemEffectiveLocationName" : "XYZ Location", + "retrievalServicePointId" : "c4c90014-c8c9-4ade-8f24-b5e313319f4b", + "retrievalServicePointName" : "Circ Desk Test" + }, "position": 1, "fulfillmentPreference": "Hold Shelf", diff --git a/ramls/request.json b/ramls/request.json index b00b5c9f2..7083f5211 100644 --- a/ramls/request.json +++ b/ramls/request.json @@ -133,6 +133,24 @@ "barcode": { "description": "barcode of the item", "type": "string" + }, + "itemEffectiveLocationId": { + "description": "Item's effective location", + "type": "string", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "itemEffectiveLocationName": { + "description": "Item's effective location name", + "type": "string" + }, + "retrievalServicePointId": { + "description": "Item's location primary service point", + "type": "string", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "retrievalServicePointName": { + "description": "Item's location primary service point name", + "type": "string" } }, "additionalProperties": false diff --git a/src/main/resources/templates/db_scripts/schema.json b/src/main/resources/templates/db_scripts/schema.json index bc1b06ae2..066c0bf1f 100644 --- a/src/main/resources/templates/db_scripts/schema.json +++ b/src/main/resources/templates/db_scripts/schema.json @@ -235,6 +235,30 @@ "tOps": "ADD", "caseSensitive": false, "removeAccents": false + }, + { + "fieldName": "item.itemEffectiveLocationId", + "tOps": "ADD", + "caseSensitive": false, + "removeAccents": false + }, + { + "fieldName": "item.itemEffectiveLocationName", + "tOps": "ADD", + "caseSensitive": false, + "removeAccents": false + }, + { + "fieldName": "item.retrievalServicePointId", + "tOps": "ADD", + "caseSensitive": false, + "removeAccents": false + }, + { + "fieldName": "item.retrievalServicePointName", + "tOps": "ADD", + "caseSensitive": false, + "removeAccents": false } ], "fullTextIndex": [ @@ -298,6 +322,30 @@ "tOps": "ADD", "caseSensitive": false, "removeAccents": false + }, + { + "fieldName": "item.itemEffectiveLocationId", + "tOps": "ADD", + "caseSensitive": false, + "removeAccents": false + }, + { + "fieldName": "item.itemEffectiveLocationName", + "tOps": "ADD", + "caseSensitive": false, + "removeAccents": false + }, + { + "fieldName": "item.retrievalServicePointId", + "tOps": "ADD", + "caseSensitive": false, + "removeAccents": false + }, + { + "fieldName": "item.retrievalServicePointName", + "tOps": "ADD", + "caseSensitive": false, + "removeAccents": false } ], "customSnippetPath": "requestUpdateTrigger.sql" diff --git a/src/test/java/org/folio/rest/api/RequestsApiTest.java b/src/test/java/org/folio/rest/api/RequestsApiTest.java index c5ad531cb..465b299bc 100644 --- a/src/test/java/org/folio/rest/api/RequestsApiTest.java +++ b/src/test/java/org/folio/rest/api/RequestsApiTest.java @@ -41,6 +41,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; +import java.util.Collections; import java.util.List; import java.util.UUID; import java.util.concurrent.CompletableFuture; @@ -126,6 +127,10 @@ public void canCreateARequest(String requestLevel) throws InterruptedException, UUID holdingsRecordId = UUID.randomUUID(); UUID instanceId = UUID.randomUUID(); UUID pickupServicePointId = UUID.randomUUID(); + String itemEffectiveLocationId = UUID.randomUUID().toString(); + String itemEffectiveLocationName = "Book"; + String retrievalServicePointId = UUID.randomUUID().toString(); + String retrievalServicePointName = "SP-1"; DateTime requestDate = new DateTime(2017, 7, 22, 10, 22, 54, DateTimeZone.UTC); DateTime requestExpirationDate = new DateTime(2017, 7, 30, 0, 0, DateTimeZone.UTC); DateTime holdShelfExpirationDate = new DateTime(2017, 8, 31, 0, 0, DateTimeZone.UTC); @@ -133,7 +138,9 @@ public void canCreateARequest(String requestLevel) throws InterruptedException, UUID isbnIdentifierId = UUID.randomUUID(); UUID issnIdentifierId = UUID.randomUUID(); - final RequestItemSummary nod = new RequestItemSummary("Nod", "565578437802") + final RequestItemSummary nod = new RequestItemSummary("Nod", + "565578437802", Collections.emptyList(), itemEffectiveLocationId, + itemEffectiveLocationName, retrievalServicePointId, retrievalServicePointName) .addIdentifier(isbnIdentifierId, "978-92-8011-566-9") .addIdentifier(issnIdentifierId, "2193988"); @@ -182,6 +189,10 @@ public void canCreateARequest(String requestLevel) throws InterruptedException, assertThat(representation.containsKey("item"), is(true)); JsonObject item = representation.getJsonObject("item"); assertThat(item.getString("barcode"), is("565578437802")); + assertThat(item.getString("itemEffectiveLocationId"), is(itemEffectiveLocationId)); + assertThat(item.getString("itemEffectiveLocationName"), is(itemEffectiveLocationName)); + assertThat(item.getString("retrievalServicePointId"), is(retrievalServicePointId)); + assertThat(item.getString("retrievalServicePointName"), is(retrievalServicePointName)); assertThat(representation.containsKey("instance"), is(true)); JsonObject instance = representation.getJsonObject("instance"); diff --git a/src/test/java/org/folio/rest/support/builders/RequestItemSummary.java b/src/test/java/org/folio/rest/support/builders/RequestItemSummary.java index 99dcfaa67..2a05eff42 100644 --- a/src/test/java/org/folio/rest/support/builders/RequestItemSummary.java +++ b/src/test/java/org/folio/rest/support/builders/RequestItemSummary.java @@ -12,15 +12,28 @@ public class RequestItemSummary { final String title; final String barcode; final List> identifiers; + final String itemEffectiveLocationId; + final String itemEffectiveLocationName; + final String retrievalServicePointId; + final String retrievalServicePointName; + public RequestItemSummary(String title, String barcode) { this(title, barcode, Collections.emptyList()); } private RequestItemSummary(String title, String barcode, List> identifiers) { + this(title, barcode, identifiers, null, null, null, null); + } + + public RequestItemSummary(String title, String barcode, List> identifiers, String itemEffectiveLocationId, String itemEffectiveLocationName, String retrievalServicePointId, String retrievalServicePointName) { this.title = title; this.barcode = barcode; this.identifiers = new ArrayList<>(identifiers); + this.itemEffectiveLocationId = itemEffectiveLocationId; + this.itemEffectiveLocationName = itemEffectiveLocationName; + this.retrievalServicePointId = retrievalServicePointId; + this.retrievalServicePointName = retrievalServicePointName; } public RequestItemSummary addIdentifier(UUID identifierId, String value) { @@ -30,6 +43,10 @@ public RequestItemSummary addIdentifier(UUID identifierId, String value) { return new RequestItemSummary( this.title, this.barcode, - copiedIdentifiers); + copiedIdentifiers, + this.itemEffectiveLocationId, + this.itemEffectiveLocationName, + this.retrievalServicePointId, + this.retrievalServicePointName); } } diff --git a/src/test/java/org/folio/rest/support/builders/RequestRequestBuilder.java b/src/test/java/org/folio/rest/support/builders/RequestRequestBuilder.java index 1fc2c7cd3..dfc32ba92 100644 --- a/src/test/java/org/folio/rest/support/builders/RequestRequestBuilder.java +++ b/src/test/java/org/folio/rest/support/builders/RequestRequestBuilder.java @@ -1,18 +1,16 @@ package org.folio.rest.support.builders; -import java.util.UUID; -import java.util.stream.Collectors; - import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import lombok.AllArgsConstructor; import lombok.With; - import org.folio.rest.jaxrs.model.SearchIndex; +import org.folio.rest.jaxrs.model.Tags; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; -import org.folio.rest.jaxrs.model.Tags; +import java.util.UUID; +import java.util.stream.Collectors; @AllArgsConstructor @With @@ -111,6 +109,11 @@ public JsonObject create() { final JsonObject item = new JsonObject(); put(item, "barcode", this.itemSummary.barcode); + put(item, "itemEffectiveLocationId", this.itemSummary.itemEffectiveLocationId); + put(item, "itemEffectiveLocationName", this.itemSummary.itemEffectiveLocationName); + put(item, "retrievalServicePointId", this.itemSummary.retrievalServicePointId); + put(item, "retrievalServicePointName", this.itemSummary.retrievalServicePointName); + final JsonArray identifiers = new JsonArray(this.itemSummary.identifiers .stream() .map(pair -> new JsonObject() From 6818a82cde36b86128fa2da987875478f6d49f0d Mon Sep 17 00:00:00 2001 From: Kapil Verma <85541312+kapil-epam@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:05:41 +0530 Subject: [PATCH 2/7] CIRCSTORE-540: Extended ItemUpdateProcessorForRequest to sync item location and SP updates in Request records (#503) * CIRCSTORE-540: ItemUpdateProcessorForRequest update item's location and SP * CIRCSTORE-540: added log for debugging * CIRCSTORE-540: added log for debugging * CIRCSTORE-540: set additionalProperties true inside location.json * CIRCSTORE-540: fix exception issue * CIRCSTORE-540: fix exception issue * CIRCSTORE-540: 1) added async mechanism in EventProcessor class to execute and support non-blocking api calls. 2) Added test case. * CIRCSTORE-540: removed unnecessary log and code * CIRCSTORE-540: using location and SP constants * CIRCSTORE-540: fix NLP due to logging of null object * CIRCSTORE-540: fixed sonar issues * CIRCSTORE-540: added interface dependency for location and service-point api --- descriptors/ModuleDescriptor-template.json | 8 ++ ramls/locations/location.json | 67 ++++++++++++++ ramls/locations/locations.json | 24 +++++ ramls/request-storage.raml | 2 + .../rest/client/InventoryStorageClient.java | 8 ++ .../event/handler/ItemUpdateEventHandler.java | 5 +- .../handler/processor/EventProcessor.java | 20 +++-- .../ItemUpdateProcessorForRequest.java | 87 ++++++++++++++++++- ...ePointDeleteProcessorForRequestPolicy.java | 5 +- ...ServicePointUpdateProcessorForRequest.java | 5 +- ...ePointUpdateProcessorForRequestPolicy.java | 5 +- .../rest/api/EventConsumerVerticleTest.java | 82 ++++++++++++++++- .../support/builders/LocationBuilder.java | 30 +++++++ 13 files changed, 327 insertions(+), 21 deletions(-) create mode 100644 ramls/locations/location.json create mode 100644 ramls/locations/locations.json create mode 100644 src/test/java/org/folio/rest/support/builders/LocationBuilder.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 4ce7a918e..69bce0105 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -29,6 +29,14 @@ { "id": "configuration", "version": "2.0" + }, + { + "id": "locations", + "version": "3.1" + }, + { + "id": "service-points", + "version": "3.4" } ], "provides": [ diff --git a/ramls/locations/location.json b/ramls/locations/location.json new file mode 100644 index 000000000..31a13886a --- /dev/null +++ b/ramls/locations/location.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "A (shelf) location, the forth-level location unit below institution, campus, and library.", + "javaType": "org.folio.rest.jaxrs.model.Location", + "type": "object", + "properties": { + "id": { + "description": "id of this (shelf) location record as UUID.", + "type": "string" + }, + "name": { + "description": "Name of the (shelf) location", + "type": "string" + }, + "code": { + "description": "Code of the (shelf) location, usually an abbreviation of the name.", + "type": "string" + }, + "description": { + "description": "Description of the (shelf) location.", + "type": "string" + }, + "discoveryDisplayName": { + "description": "Name of the (shelf) location to be shown in the discovery.", + "type": "string" + }, + "isActive": { + "description": "Whether this (shelf) location is active. Inactive (shelf) locations can no longer been used.", + "type": "boolean" + }, + "institutionId": { + "description": "The UUID of the institution, the first-level location unit, this (shelf) location belongs to.", + "type": "string" + }, + "campusId": { + "description": "The UUID of the campus, the second-level location unit, this (shelf) location belongs to.", + "type": "string" + }, + "libraryId": { + "description": "The UUID of the library, the third-level location unit, this (shelf) location belongs to.", + "type": "string" + }, + "primaryServicePoint": { + "description": "The UUID of the primary service point of this (shelf) location.", + "format": "uuid", + "type": "string" + }, + "servicePointIds": { + "description": "All service points that this (shelf) location has.", + "type": "array", + "items": { + "description": "The UUID of a service point that belongs to this (shelf) location.", + "type": "string", + "format": "uuid", + "not": { + "type": "null" + } + } + }, + "metadata": { + "type": "object", + "$ref": "../raml-util/schemas/metadata.schema", + "readonly": true + } + }, + "additionalProperties": true +} diff --git a/ramls/locations/locations.json b/ramls/locations/locations.json new file mode 100644 index 000000000..df6bdad1d --- /dev/null +++ b/ramls/locations/locations.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "List of (shelf) locations.", + "type": "object", + "properties": { + "locations": { + "id": "locations", + "description": "List of (shelf) locations.", + "type": "array", + "items": { + "type": "object", + "$ref": "location.json" + } + }, + "totalRecords": { + "description": "Estimated or exact total number of records", + "type": "integer" + } + }, + "required": [ + "locations", + "totalRecords" + ] +} diff --git a/ramls/request-storage.raml b/ramls/request-storage.raml index cf32c0fc7..29f93e5f4 100644 --- a/ramls/request-storage.raml +++ b/ramls/request-storage.raml @@ -13,6 +13,8 @@ types: requests: !include requests.json errors: !include raml-util/schemas/errors.schema parameters: !include raml-util/schemas/parameters.schema + location: !include locations/location.json + locations: !include locations/locations.json traits: pageable: !include raml-util/traits/pageable.raml diff --git a/src/main/java/org/folio/rest/client/InventoryStorageClient.java b/src/main/java/org/folio/rest/client/InventoryStorageClient.java index b9a830755..2d607c470 100644 --- a/src/main/java/org/folio/rest/client/InventoryStorageClient.java +++ b/src/main/java/org/folio/rest/client/InventoryStorageClient.java @@ -3,6 +3,7 @@ import java.util.Collection; import java.util.Map; +import org.folio.rest.jaxrs.model.Location; import org.folio.rest.jaxrs.model.Servicepoint; import io.vertx.core.Future; @@ -12,6 +13,9 @@ public class InventoryStorageClient extends OkapiClient { private static final String SERVICE_POINTS_URL = "/service-points"; private static final String SERVICE_POINTS_COLLECTION_NAME = "servicepoints"; + private static final String LOCATION_URL = "/locations"; + private static final String LOCATION_COLLECTION_NAME = "locations"; + public InventoryStorageClient(Vertx vertx, Map okapiHeaders) { super(vertx, okapiHeaders); } @@ -20,4 +24,8 @@ public Future> getServicePoints(Collection ids) return get(SERVICE_POINTS_URL, ids, SERVICE_POINTS_COLLECTION_NAME, Servicepoint.class); } + public Future> getLocations(Collection ids) { + return get(LOCATION_URL, ids, LOCATION_COLLECTION_NAME, Location.class); + } + } diff --git a/src/main/java/org/folio/service/event/handler/ItemUpdateEventHandler.java b/src/main/java/org/folio/service/event/handler/ItemUpdateEventHandler.java index c27bc6d96..3535af48e 100644 --- a/src/main/java/org/folio/service/event/handler/ItemUpdateEventHandler.java +++ b/src/main/java/org/folio/service/event/handler/ItemUpdateEventHandler.java @@ -5,6 +5,7 @@ import org.apache.commons.collections4.map.CaseInsensitiveMap; import org.folio.kafka.AsyncRecordHandler; import org.folio.persist.RequestRepository; +import org.folio.rest.client.InventoryStorageClient; import org.folio.service.event.handler.processor.ItemUpdateProcessorForRequest; import io.vertx.core.Context; @@ -14,7 +15,6 @@ public class ItemUpdateEventHandler implements AsyncRecordHandler { private final Context context; - public ItemUpdateEventHandler(Context context) { this.context = context; } @@ -24,9 +24,8 @@ public Future handle(KafkaConsumerRecord kafkaConsumerRe JsonObject payload = new JsonObject(kafkaConsumerRecord.value()); CaseInsensitiveMap headers = new CaseInsensitiveMap<>(kafkaHeadersToMap(kafkaConsumerRecord.headers())); - ItemUpdateProcessorForRequest itemUpdateProcessorForRequest = - new ItemUpdateProcessorForRequest(new RequestRepository(context, headers)); + new ItemUpdateProcessorForRequest(new RequestRepository(context, headers), new InventoryStorageClient(context.owner(), headers)); return itemUpdateProcessorForRequest.run(kafkaConsumerRecord.key(), payload); } diff --git a/src/main/java/org/folio/service/event/handler/processor/EventProcessor.java b/src/main/java/org/folio/service/event/handler/processor/EventProcessor.java index 111d91524..d3b1fad83 100644 --- a/src/main/java/org/folio/service/event/handler/processor/EventProcessor.java +++ b/src/main/java/org/folio/service/event/handler/processor/EventProcessor.java @@ -49,20 +49,24 @@ private Future> processEvent(JsonObject payload) { return succeededFuture(); } - List> relevantChanges = collectRelevantChanges(payload); + Future>> relevantChangesFuture = + collectRelevantChanges(payload); - if (relevantChanges.isEmpty()) { - log.info("processEvent:: no relevant changes detected"); - return succeededFuture(); - } + return relevantChangesFuture.compose(relevantChanges -> { + + if (relevantChanges.isEmpty()) { + log.info("processEvent:: no relevant changes detected"); + return succeededFuture(); + } - log.info("processEvent:: {} relevant changes detected, applying", relevantChanges::size); - return applyChanges(relevantChanges, payload); + log.info("processEvent:: {} relevant changes detected, applying", relevantChanges::size); + return applyChanges(relevantChanges, payload); + }); } protected abstract boolean validatePayload(JsonObject payload); - protected abstract List> collectRelevantChanges(JsonObject payload); + protected abstract Future>> collectRelevantChanges(JsonObject payload); private Future> applyChanges(List> changes, JsonObject payload) { log.debug("applyChanges:: payload: {}", payload); diff --git a/src/main/java/org/folio/service/event/handler/processor/ItemUpdateProcessorForRequest.java b/src/main/java/org/folio/service/event/handler/processor/ItemUpdateProcessorForRequest.java index 26266f056..1fd662c0a 100644 --- a/src/main/java/org/folio/service/event/handler/processor/ItemUpdateProcessorForRequest.java +++ b/src/main/java/org/folio/service/event/handler/processor/ItemUpdateProcessorForRequest.java @@ -1,16 +1,28 @@ package org.folio.service.event.handler.processor; +import static io.vertx.core.Future.succeededFuture; import static org.apache.commons.lang3.ObjectUtils.notEqual; import static org.folio.service.event.InventoryEventType.INVENTORY_ITEM_UPDATED; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; +import io.vertx.core.Future; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.persist.RequestRepository; +import org.folio.rest.client.InventoryStorageClient; import org.folio.rest.jaxrs.model.CallNumberComponents; +import org.folio.rest.jaxrs.model.Item; +import org.folio.rest.jaxrs.model.Location; import org.folio.rest.jaxrs.model.Request; +import org.folio.rest.jaxrs.model.Servicepoint; import org.folio.rest.persist.Criteria.Criteria; import org.folio.rest.persist.Criteria.Criterion; @@ -19,17 +31,26 @@ public class ItemUpdateProcessorForRequest extends UpdateEventProcessor { private static final Logger log = LogManager.getLogger(ItemUpdateProcessorForRequest.class); + private final InventoryStorageClient inventoryStorageClient; + private static final String EFFECTIVE_SHELVING_ORDER_KEY = "effectiveShelvingOrder"; private static final String EFFECTIVE_CALL_NUMBER_COMPONENTS_KEY = "effectiveCallNumberComponents"; private static final String CALL_NUMBER_KEY = "callNumber"; private static final String CALL_NUMBER_PREFIX_KEY = "prefix"; private static final String CALL_NUMBER_SUFFIX_KEY = "suffix"; - public ItemUpdateProcessorForRequest(RequestRepository repository) { + public static final String ITEM_EFFECTIVE_LOCATION_ID = "itemEffectiveLocationId"; + public static final String ITEM_EFFECTIVE_LOCATION_NAME = "itemEffectiveLocationName"; + public static final String RETRIEVAL_SERVICE_POINT_ID = "retrievalServicePointId"; + public static final String RETRIEVAL_SERVICE_POINT_NAME = "retrievalServicePointName"; + + public ItemUpdateProcessorForRequest(RequestRepository repository, InventoryStorageClient inventoryStorageClient) { super(INVENTORY_ITEM_UPDATED, repository); + this.inventoryStorageClient = inventoryStorageClient; } - protected List> collectRelevantChanges(JsonObject payload) { + @Override + protected Future>> collectRelevantChanges(JsonObject payload) { JsonObject oldObject = payload.getJsonObject("old"); JsonObject newObject = payload.getJsonObject("new"); @@ -50,9 +71,69 @@ protected List> collectRelevantChanges(JsonObject payload) { changes.add(new Change<>(request -> request.getSearchIndex().setShelvingOrder(newShelvingOrder))); } - return changes; + Future> fetchLocationAndServicePoint = updateItemAndServicePoint(newObject); + return fetchLocationAndServicePoint + .compose(locationAndSpData -> addLocationAndServicePointChanges(locationAndSpData, changes)) + .compose(r -> Future.succeededFuture(changes)) + .recover(throwable -> Future.succeededFuture(changes)); + } + + private static Future>> addLocationAndServicePointChanges(Map locationAndSpData, List> changes) { + log.info("ItemUpdateProcessorForRequest :: locationAndSpData: {}", locationAndSpData); + changes.add(new Change<>(request -> { + if (request.getItem() == null) { + request.setItem(new Item()); + } + request.getItem().setItemEffectiveLocationId(locationAndSpData.get(ITEM_EFFECTIVE_LOCATION_ID)); + request.getItem().setItemEffectiveLocationName(locationAndSpData.get(ITEM_EFFECTIVE_LOCATION_NAME)); + request.getItem().setRetrievalServicePointId(locationAndSpData.get(RETRIEVAL_SERVICE_POINT_ID)); + request.getItem().setRetrievalServicePointName(locationAndSpData.get(RETRIEVAL_SERVICE_POINT_NAME)); + })); + return Future.succeededFuture(changes); + } + + private Future> updateItemAndServicePoint(JsonObject newObject) { + String effectiveLocationId = newObject.getString("effectiveLocationId"); + Map locationAndSpData = new HashMap<>(); + locationAndSpData.put(ITEM_EFFECTIVE_LOCATION_ID, effectiveLocationId); + return inventoryStorageClient.getLocations(Collections.singletonList(effectiveLocationId)) + .compose(locations -> setEffectiveLocationData(locations, effectiveLocationId, locationAndSpData)) + .compose(primaryServicePoint -> setRetrievalServicePointData(primaryServicePoint, locationAndSpData)) + .compose(e -> Future.succeededFuture(locationAndSpData)) + .onFailure(throwable -> log.info("ItemUpdateProcessorForRequest :: Error while fetching Locations: {}", throwable.toString())); + } + + private static Future setEffectiveLocationData(Collection locations, String effectiveLocationId, + Map locationAndSpData) { + Location effectiveLocation = locations.stream() + .filter(l -> l.getId().equals(effectiveLocationId)) + .findFirst().orElse(null); + if (Objects.nonNull(effectiveLocation)) { + locationAndSpData.put(ITEM_EFFECTIVE_LOCATION_NAME, effectiveLocation.getName()); + return succeededFuture(effectiveLocation.getPrimaryServicePoint().toString()); + } + return succeededFuture(); + } + + private Future setRetrievalServicePointData(String primaryServicePoint, Map locationAndSpData) { + if (!StringUtils.isBlank(primaryServicePoint)) { + locationAndSpData.put(RETRIEVAL_SERVICE_POINT_ID, primaryServicePoint); + return inventoryStorageClient.getServicePoints(Collections.singletonList(primaryServicePoint)) + .compose(servicePoints -> { + Servicepoint retrievalServicePoint = servicePoints.stream() + .filter(sp -> sp.getId().equals(primaryServicePoint)) + .findFirst().orElse(null); + if (Objects.nonNull(retrievalServicePoint)) { + locationAndSpData.put(RETRIEVAL_SERVICE_POINT_NAME, retrievalServicePoint.getName()); + } + return succeededFuture(); + }).onFailure(throwable -> log.info("ItemUpdateProcessorForRequest :: Error while fetching ServicePoint: {}", + throwable.toString())); + } + return succeededFuture(); } + @Override protected Criterion criterionForObjectsToBeUpdated(String oldObjectId) { log.info("criteriaForObjectsToBeUpdated:: oldObjectId: {}", oldObjectId); diff --git a/src/main/java/org/folio/service/event/handler/processor/ServicePointDeleteProcessorForRequestPolicy.java b/src/main/java/org/folio/service/event/handler/processor/ServicePointDeleteProcessorForRequestPolicy.java index 9ab5d3576..9ae792ce4 100644 --- a/src/main/java/org/folio/service/event/handler/processor/ServicePointDeleteProcessorForRequestPolicy.java +++ b/src/main/java/org/folio/service/event/handler/processor/ServicePointDeleteProcessorForRequestPolicy.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; +import io.vertx.core.Future; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.persist.RequestPolicyRepository; @@ -27,7 +28,7 @@ public ServicePointDeleteProcessorForRequestPolicy( } @Override - protected List> collectRelevantChanges(JsonObject payload) { + protected Future>> collectRelevantChanges(JsonObject payload) { log.debug("collectRelevantChanges:: payload: {}", payload); JsonObject oldObject = payload.getJsonObject("old"); @@ -38,7 +39,7 @@ protected List> collectRelevantChanges(JsonObject payload) changes.add(new Change<>(requestPolicy -> removeServicePointFromRequestPolicy(requestPolicy, deletedServicePointId))); - return changes; + return Future.succeededFuture(changes); } @Override diff --git a/src/main/java/org/folio/service/event/handler/processor/ServicePointUpdateProcessorForRequest.java b/src/main/java/org/folio/service/event/handler/processor/ServicePointUpdateProcessorForRequest.java index 34e30bf64..4054d6b4b 100644 --- a/src/main/java/org/folio/service/event/handler/processor/ServicePointUpdateProcessorForRequest.java +++ b/src/main/java/org/folio/service/event/handler/processor/ServicePointUpdateProcessorForRequest.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.List; +import io.vertx.core.Future; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.persist.RequestRepository; @@ -24,7 +25,7 @@ public ServicePointUpdateProcessorForRequest(RequestRepository requestRepository } @Override - protected List> collectRelevantChanges(JsonObject payload) { + protected Future>> collectRelevantChanges(JsonObject payload) { JsonObject oldObject = payload.getJsonObject("old"); JsonObject newObject = payload.getJsonObject("new"); @@ -40,7 +41,7 @@ protected List> collectRelevantChanges(JsonObject payload) { .setPickupServicePointName(newServicePointName))); } - return changes; + return Future.succeededFuture(changes); } @Override diff --git a/src/main/java/org/folio/service/event/handler/processor/ServicePointUpdateProcessorForRequestPolicy.java b/src/main/java/org/folio/service/event/handler/processor/ServicePointUpdateProcessorForRequestPolicy.java index e6cb8aad9..33a3db946 100644 --- a/src/main/java/org/folio/service/event/handler/processor/ServicePointUpdateProcessorForRequestPolicy.java +++ b/src/main/java/org/folio/service/event/handler/processor/ServicePointUpdateProcessorForRequestPolicy.java @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.List; +import io.vertx.core.Future; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.persist.RequestPolicyRepository; @@ -28,7 +29,7 @@ public ServicePointUpdateProcessorForRequestPolicy( } @Override - protected List> collectRelevantChanges(JsonObject payload) { + protected Future>> collectRelevantChanges(JsonObject payload) { JsonObject oldObject = payload.getJsonObject("old"); JsonObject newObject = payload.getJsonObject("new"); List> changes = new ArrayList<>(); @@ -44,7 +45,7 @@ protected List> collectRelevantChanges(JsonObject payload) updatedServicePointId))); } - return changes; + return Future.succeededFuture(changes); } @Override diff --git a/src/test/java/org/folio/rest/api/EventConsumerVerticleTest.java b/src/test/java/org/folio/rest/api/EventConsumerVerticleTest.java index 81dd93644..e91a8c9fd 100644 --- a/src/test/java/org/folio/rest/api/EventConsumerVerticleTest.java +++ b/src/test/java/org/folio/rest/api/EventConsumerVerticleTest.java @@ -11,14 +11,22 @@ import static org.folio.kafka.services.KafkaEnvironmentProperties.environment; import static org.folio.kafka.services.KafkaEnvironmentProperties.host; import static org.folio.kafka.services.KafkaEnvironmentProperties.port; +import static org.folio.rest.RestVerticle.OKAPI_HEADER_TOKEN; +import static org.folio.rest.api.StorageTestSuite.PROXY_PORT; import static org.folio.rest.api.StorageTestSuite.TENANT_ID; import static org.folio.rest.api.StorageTestSuite.getVertx; import static org.folio.rest.support.builders.RequestRequestBuilder.OPEN_NOT_YET_FILLED; import static org.folio.rest.tools.utils.ModuleName.getModuleName; import static org.folio.rest.tools.utils.ModuleName.getModuleVersion; +import static org.folio.rest.util.OkapiConnectionParams.OKAPI_TENANT_HEADER; +import static org.folio.rest.util.OkapiConnectionParams.OKAPI_URL_HEADER; import static org.folio.service.event.InventoryEventType.INVENTORY_ITEM_UPDATED; import static org.folio.service.event.InventoryEventType.INVENTORY_SERVICE_POINT_DELETED; import static org.folio.service.event.InventoryEventType.INVENTORY_SERVICE_POINT_UPDATED; +import static org.folio.service.event.handler.processor.ItemUpdateProcessorForRequest.ITEM_EFFECTIVE_LOCATION_ID; +import static org.folio.service.event.handler.processor.ItemUpdateProcessorForRequest.ITEM_EFFECTIVE_LOCATION_NAME; +import static org.folio.service.event.handler.processor.ItemUpdateProcessorForRequest.RETRIEVAL_SERVICE_POINT_ID; +import static org.folio.service.event.handler.processor.ItemUpdateProcessorForRequest.RETRIEVAL_SERVICE_POINT_NAME; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.hasItems; @@ -27,6 +35,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import java.net.URL; +import java.util.Base64; import java.util.List; import java.util.Map; import java.util.Optional; @@ -43,6 +52,7 @@ import org.folio.rest.support.ApiTests; import org.folio.rest.support.builders.ItemBuilder; import org.folio.rest.support.builders.ItemBuilder.ItemCallNumberComponents; +import org.folio.rest.support.builders.LocationBuilder; import org.folio.rest.support.builders.RequestItemSummary; import org.folio.rest.support.builders.RequestPolicyBuilder; import org.folio.rest.support.builders.RequestRequestBuilder; @@ -169,6 +179,38 @@ public void requestSearchIndexIsNotUpdatedWhenEventContainsNoRelevantChanges() { verifyRequestSearchIndex(REQUEST_ID, searchIndex); } + @Test + public void requestItemLocationAndSpIsSetWhenItemEffectiveLocationChange() { + JsonObject oldItem = buildItem(DEFAULT_CALL_NUMBER_PREFIX, DEFAULT_CALL_NUMBER, + DEFAULT_CALL_NUMBER_SUFFIX, DEFAULT_SHELVING_ORDER); + JsonObject newItem = oldItem.copy(); + + //Creating and mocking ServicePoint + String servicePointId = UUID.randomUUID().toString(); + JsonObject servicePoint = buildServicePoint(servicePointId, "ServicePoint-1"); + createStubForServicePoints(List.of(servicePoint)); + + //Creating and mocking Location + String locationId = UUID.randomUUID().toString(); + JsonObject location = buildLocation(locationId, "Location-1", servicePointId); + createStubForLocations(List.of(location)); + newItem.put("effectiveLocationId", locationId); + + // Expected item + JsonObject expectedItem = new JsonObject(); + expectedItem.put(ITEM_EFFECTIVE_LOCATION_ID, locationId); + expectedItem.put(ITEM_EFFECTIVE_LOCATION_NAME, location.getString("name")); + expectedItem.put(RETRIEVAL_SERVICE_POINT_ID, servicePointId); + expectedItem.put(RETRIEVAL_SERVICE_POINT_NAME, servicePoint.getString("name")); + + createRequest(buildRequest(REQUEST_ID, oldItem)); + + int initialOffset = getOffsetForItemUpdateEvents(); + publishItemUpdateEvent(oldItem, newItem); + waitUntilValueIsIncreased(initialOffset, EventConsumerVerticleTest::getOffsetForItemUpdateEvents); + verifyRequestItem(REQUEST_ID, expectedItem); + } + @Test public void requestSearchIndexIsNotUpdatedWhenRequestAndEventAreForDifferentItems() { JsonObject oldItem = buildItem(DEFAULT_CALL_NUMBER_PREFIX, DEFAULT_CALL_NUMBER, @@ -486,6 +528,18 @@ private static JsonObject buildItem(String prefix, String callNumber, String suf .create(); } + private static JsonObject buildLocation(String id, String name, String primaryServicePoint) { + return buildLocation(id, name, "code", primaryServicePoint); + } + + private static JsonObject buildLocation(String id, String name, String code, String primaryServicePoint) { + return new LocationBuilder(name) + .withId(id) + .withCode(code) + .withPrimaryServicePoint(primaryServicePoint) + .create(); + } + private static JsonObject buildServicePoint(String id, String name) { return buildServicePoint(id, name, "code"); } @@ -592,7 +646,9 @@ private void publishInvalidServicePointDeleteEvent(JsonObject servicePoint) { private void publishEvent(String topic, JsonObject eventPayload) { var record = KafkaProducerRecord.create(topic, "test-key", eventPayload); - record.addHeader("X-Okapi-Tenant", TENANT_ID); + record.addHeader(OKAPI_TENANT_HEADER, TENANT_ID); + record.addHeader(OKAPI_URL_HEADER, "http://localhost:" + PROXY_PORT); + record.addHeader(OKAPI_HEADER_TOKEN, "RANDOM_TOKEN"); waitFor(producer.write(record)); } @@ -638,6 +694,19 @@ private JsonObject verifyRequestSearchIndex(String requestId, SearchIndex search .until(() -> getRequestSearchIndex(requestId), equalTo(mapFrom(searchIndex))); } + private JsonObject verifyRequestItem(String requestId, JsonObject itemObject) { + return waitAtMost(60, SECONDS) + .until(() -> { + JsonObject requestItem = getRequestItem(requestId); + JsonObject actualItem = new JsonObject(); + actualItem.put(ITEM_EFFECTIVE_LOCATION_ID, requestItem.getString(ITEM_EFFECTIVE_LOCATION_ID)); + actualItem.put(ITEM_EFFECTIVE_LOCATION_NAME, requestItem.getString(ITEM_EFFECTIVE_LOCATION_NAME)); + actualItem.put(RETRIEVAL_SERVICE_POINT_ID, requestItem.getString(RETRIEVAL_SERVICE_POINT_ID)); + actualItem.put(RETRIEVAL_SERVICE_POINT_NAME, requestItem.getString(RETRIEVAL_SERVICE_POINT_NAME)); + return actualItem; + }, equalTo(itemObject)); + } + private JsonObject verifyPickupServicePointName(String requestId, SearchIndex searchIndex) { return waitAtMost(60, SECONDS) .until(() -> getRequestSearchIndex(requestId), equalTo(mapFrom(searchIndex))); @@ -668,6 +737,10 @@ private JsonObject getRequestSearchIndex(String requestId) { return getRequest(requestId).getJsonObject("searchIndex"); } + private JsonObject getRequestItem(String requestId) { + return getRequest(requestId).getJsonObject("item"); + } + private static JsonObject buildRequestPolicy(String requestPolicyId, List requestTypes, AllowedServicePoints allowedServicePoints) { @@ -763,6 +836,13 @@ private void createStubForServicePoints(List servicePoints) { .encodePrettily()))); } + private void createStubForLocations(List locations) { + StorageTestSuite.getWireMockServer() + .stubFor(WireMock.get(urlPathMatching("/locations.*")) + .willReturn(ok().withBody(new JsonObject().put("locations", new JsonArray(locations)) + .encodePrettily()))); + } + private JsonObject buildRequestPolicy(List allowedServicePointIds, RequestType... requestTypes) { diff --git a/src/test/java/org/folio/rest/support/builders/LocationBuilder.java b/src/test/java/org/folio/rest/support/builders/LocationBuilder.java new file mode 100644 index 000000000..fe50b6c72 --- /dev/null +++ b/src/test/java/org/folio/rest/support/builders/LocationBuilder.java @@ -0,0 +1,30 @@ +package org.folio.rest.support.builders; + +import io.vertx.core.json.JsonObject; +import lombok.AllArgsConstructor; +import lombok.With; + +import java.util.UUID; + +@With +@AllArgsConstructor +public class LocationBuilder extends JsonBuilder implements Builder { + private final String id; + private final String name; + private final String code; + private final String primaryServicePoint; + + public LocationBuilder(String name) { + this(UUID.randomUUID().toString(), name, null, null); + } + + @Override + public JsonObject create() { + JsonObject location = new JsonObject(); + put(location, "id", this.id); + put(location, "name", this.name); + put(location, "code", this.code); + put(location, "primaryServicePoint", this.primaryServicePoint); + return location; + } +} From bec93d2e5467e2f5e9805e0e2cd438ab9537d243 Mon Sep 17 00:00:00 2001 From: Kapil Verma <85541312+kapil-epam@users.noreply.github.com> Date: Mon, 23 Dec 2024 15:40:50 +0530 Subject: [PATCH 3/7] CIRCSTORE-541: added ItemRetrievalServicePointUpdateProcessorForRequest for retrival SP for request (#505) * CIRCSTORE-540: ItemUpdateProcessorForRequest update item's location and SP * CIRCSTORE-540: added log for debugging * CIRCSTORE-540: added log for debugging * CIRCSTORE-540: set additionalProperties true inside location.json * CIRCSTORE-540: fix exception issue * CIRCSTORE-540: fix exception issue * CIRCSTORE-540: 1) added async mechanism in EventProcessor class to execute and support non-blocking api calls. 2) Added test case. * CIRCSTORE-540: removed unnecessary log and code * CIRCSTORE-540: using location and SP constants * CIRCSTORE-540: fix NLP due to logging of null object * CIRCSTORE-540: fixed sonar issues * CIRCSTORE-541: added ItemRetrievalServicePointUpdateProcessorForRequest for retrival SP for request * CIRCSTORE-541: fix duplicate code line sonar issue * CIRCSTORE-540: added interface dependency for location and service-point api * CIRCSTORE-541: resolved merge conflict --- .../ServicePointUpdateEventHandler.java | 5 +- ...ServicePointUpdateProcessorForRequest.java | 55 +++++++++++++++++++ .../rest/api/EventConsumerVerticleTest.java | 49 ++++++++++++++++- 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/folio/service/event/handler/processor/ItemRetrievalServicePointUpdateProcessorForRequest.java diff --git a/src/main/java/org/folio/service/event/handler/ServicePointUpdateEventHandler.java b/src/main/java/org/folio/service/event/handler/ServicePointUpdateEventHandler.java index 27c4ff909..2f5fe316d 100644 --- a/src/main/java/org/folio/service/event/handler/ServicePointUpdateEventHandler.java +++ b/src/main/java/org/folio/service/event/handler/ServicePointUpdateEventHandler.java @@ -6,6 +6,7 @@ import org.folio.kafka.AsyncRecordHandler; import org.folio.persist.RequestPolicyRepository; import org.folio.persist.RequestRepository; +import org.folio.service.event.handler.processor.ItemRetrievalServicePointUpdateProcessorForRequest; import org.folio.service.event.handler.processor.ServicePointUpdateProcessorForRequest; import org.folio.service.event.handler.processor.ServicePointUpdateProcessorForRequestPolicy; @@ -31,6 +32,8 @@ public Future handle(KafkaConsumerRecord kafkaConsumerRe return new ServicePointUpdateProcessorForRequest(requestRepository) .run(kafkaConsumerRecord.key(), payload) .compose(notUsed -> new ServicePointUpdateProcessorForRequestPolicy(requestPolicyRepository) - .run(kafkaConsumerRecord.key(), payload)); + .run(kafkaConsumerRecord.key(), payload)) + .compose(notUsed -> new ItemRetrievalServicePointUpdateProcessorForRequest(requestRepository) + .run(kafkaConsumerRecord.key(), payload)); } } diff --git a/src/main/java/org/folio/service/event/handler/processor/ItemRetrievalServicePointUpdateProcessorForRequest.java b/src/main/java/org/folio/service/event/handler/processor/ItemRetrievalServicePointUpdateProcessorForRequest.java new file mode 100644 index 000000000..faea6b366 --- /dev/null +++ b/src/main/java/org/folio/service/event/handler/processor/ItemRetrievalServicePointUpdateProcessorForRequest.java @@ -0,0 +1,55 @@ +package org.folio.service.event.handler.processor; + +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.persist.RequestRepository; +import org.folio.rest.jaxrs.model.Request; +import org.folio.rest.persist.Criteria.Criteria; +import org.folio.rest.persist.Criteria.Criterion; + +import java.util.ArrayList; +import java.util.List; + +import static org.apache.commons.lang3.ObjectUtils.notEqual; +import static org.folio.service.event.InventoryEventType.INVENTORY_SERVICE_POINT_UPDATED; +import static org.folio.service.event.handler.processor.ItemUpdateProcessorForRequest.RETRIEVAL_SERVICE_POINT_ID; + +public class ItemRetrievalServicePointUpdateProcessorForRequest extends UpdateEventProcessor { + private static final Logger log = LogManager.getLogger(ItemRetrievalServicePointUpdateProcessorForRequest.class); + private static final String SERVICE_POINT_NAME_KEY = "name"; + + public ItemRetrievalServicePointUpdateProcessorForRequest(RequestRepository requestRepository) { + super(INVENTORY_SERVICE_POINT_UPDATED, requestRepository); + } + + @Override + protected Future>> collectRelevantChanges(JsonObject payload) { + JsonObject newObject = payload.getJsonObject("new"); + JsonObject oldObject = payload.getJsonObject("old"); + List> changes = new ArrayList<>(); + + // compare service point names + String newServicePointName = newObject.getString(SERVICE_POINT_NAME_KEY); + String oldServicePointName = oldObject.getString(SERVICE_POINT_NAME_KEY); + if (notEqual(oldServicePointName, newServicePointName)) { + log.info("ItemRetrievalServicePointUpdateProcessorForRequest :: collectRelevantChanges:: changing item.retrievalServicePointName from {} to {}", + oldServicePointName, newServicePointName); + changes.add(new Change<>(request -> request.getItem().setRetrievalServicePointName(newServicePointName))); + } + return Future.succeededFuture(changes); + } + + @Override + protected Criterion criterionForObjectsToBeUpdated(String oldObjectId) { + log.info("ItemRetrievalServicePointUpdateProcessorForRequest :: criterionForObjectsToBeUpdated:: oldObjectId: {}", + oldObjectId); + return new Criterion( + new Criteria() + .addField("'item'") + .addField(String.format("'%s'", RETRIEVAL_SERVICE_POINT_ID)) + .setOperation("=") + .setVal(oldObjectId)); + } +} diff --git a/src/test/java/org/folio/rest/api/EventConsumerVerticleTest.java b/src/test/java/org/folio/rest/api/EventConsumerVerticleTest.java index e91a8c9fd..f319fd12f 100644 --- a/src/test/java/org/folio/rest/api/EventConsumerVerticleTest.java +++ b/src/test/java/org/folio/rest/api/EventConsumerVerticleTest.java @@ -35,7 +35,6 @@ import static org.hamcrest.MatcherAssert.assertThat; import java.net.URL; -import java.util.Base64; import java.util.List; import java.util.Map; import java.util.Optional; @@ -211,6 +210,43 @@ public void requestItemLocationAndSpIsSetWhenItemEffectiveLocationChange() { verifyRequestItem(REQUEST_ID, expectedItem); } + @Test + public void requestItemSpIsSetWhenItemEffectiveLocationPrimarySpChange() { + //Creating and mocking ServicePoint + String servicePointId = UUID.randomUUID().toString(); + JsonObject servicePoint = buildServicePoint(servicePointId, "ServicePoint-1"); + createStubForServicePoints(List.of(servicePoint)); + + //Creating and mocking Location + String locationId = UUID.randomUUID().toString(); + JsonObject location = buildLocation(locationId, "Location-1", servicePointId); + createStubForLocations(List.of(location)); + + //Creating request with retrieval service-point in item object + JsonObject item = buildItem(); + JsonObject request = buildRequest(REQUEST_ID, item); + JsonObject requestItem = request.getJsonObject("item"); + requestItem.put(RETRIEVAL_SERVICE_POINT_ID, servicePointId); + requestItem.put(RETRIEVAL_SERVICE_POINT_NAME, servicePoint.getString("name")); + createRequest(request); + + int initialOffset = getOffsetForServicePointUpdateEvents(); + + //Change ServicePoint Name in stub and publish SP update event + JsonObject servicePointNew = servicePoint.copy(); + servicePointNew.put("name", "ServicePoint-2-Changed"); + createStubForServicePoints(List.of(servicePoint)); + publishServicePointUpdateEvent(servicePoint, servicePointNew); + + // Expected RETRIEVAL_SERVICE_POINT in item + JsonObject expectedItemRetrivalServicePoint = new JsonObject(); + expectedItemRetrivalServicePoint.put(RETRIEVAL_SERVICE_POINT_ID, servicePointId); + expectedItemRetrivalServicePoint.put(RETRIEVAL_SERVICE_POINT_NAME, servicePointNew.getString("name")); + + waitUntilValueIsIncreased(initialOffset, EventConsumerVerticleTest::getOffsetForServicePointUpdateEvents); + verifyRequestItemRetrievalServicePoint(REQUEST_ID, expectedItemRetrivalServicePoint); + } + @Test public void requestSearchIndexIsNotUpdatedWhenRequestAndEventAreForDifferentItems() { JsonObject oldItem = buildItem(DEFAULT_CALL_NUMBER_PREFIX, DEFAULT_CALL_NUMBER, @@ -707,6 +743,17 @@ private JsonObject verifyRequestItem(String requestId, JsonObject itemObject) { }, equalTo(itemObject)); } + private JsonObject verifyRequestItemRetrievalServicePoint(String requestId, JsonObject itemObject) { + return waitAtMost(60, SECONDS) + .until(() -> { + JsonObject requestItem = getRequestItem(requestId); + JsonObject actualItem = new JsonObject(); + actualItem.put(RETRIEVAL_SERVICE_POINT_ID, requestItem.getString(RETRIEVAL_SERVICE_POINT_ID)); + actualItem.put(RETRIEVAL_SERVICE_POINT_NAME, requestItem.getString(RETRIEVAL_SERVICE_POINT_NAME)); + return actualItem; + }, equalTo(itemObject)); + } + private JsonObject verifyPickupServicePointName(String requestId, SearchIndex searchIndex) { return waitAtMost(60, SECONDS) .until(() -> getRequestSearchIndex(requestId), equalTo(mapFrom(searchIndex))); From bd5aca15332133aeb9c6892b22718f06950b40d3 Mon Sep 17 00:00:00 2001 From: Kapil Verma <85541312+kapil-epam@users.noreply.github.com> Date: Thu, 26 Dec 2024 16:52:42 +0530 Subject: [PATCH 4/7] CIRCSTORE-556: Add new LocationUpdateKafkaProcessor to sync location with request table (#506) * CIRCSTORE-556: ItemLocationUpdateProcessorForRequest for location sync in request record * CIRCSTORE-556: removed unnecessary stub for ServicePoints in test case --- .../java/org/folio/EventConsumerVerticle.java | 4 ++ .../service/event/InventoryEventType.java | 4 +- .../handler/LocationUpdateEventHandler.java | 31 ++++++++++ ...ItemLocationUpdateProcessorForRequest.java | 59 ++++++++++++++++++ .../kafka/topic/InventoryKafkaTopic.java | 3 +- .../rest/api/EventConsumerVerticleTest.java | 60 ++++++++++++++++++- 6 files changed, 158 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/folio/service/event/handler/LocationUpdateEventHandler.java create mode 100644 src/main/java/org/folio/service/event/handler/processor/ItemLocationUpdateProcessorForRequest.java diff --git a/src/main/java/org/folio/EventConsumerVerticle.java b/src/main/java/org/folio/EventConsumerVerticle.java index c6fa60dd8..8b20de640 100644 --- a/src/main/java/org/folio/EventConsumerVerticle.java +++ b/src/main/java/org/folio/EventConsumerVerticle.java @@ -4,6 +4,7 @@ import static org.folio.rest.tools.utils.ModuleName.getModuleName; import static org.folio.rest.tools.utils.ModuleName.getModuleVersion; import static org.folio.service.event.InventoryEventType.INVENTORY_ITEM_UPDATED; +import static org.folio.service.event.InventoryEventType.INVENTORY_LOCATION_UPDATED; import static org.folio.service.event.InventoryEventType.INVENTORY_SERVICE_POINT_DELETED; import static org.folio.service.event.InventoryEventType.INVENTORY_SERVICE_POINT_UPDATED; import static org.folio.support.kafka.KafkaConfigConstants.KAFKA_ENV; @@ -26,6 +27,7 @@ import org.folio.kafka.services.KafkaTopic; import org.folio.service.event.InventoryEventType; import org.folio.service.event.handler.ItemUpdateEventHandler; +import org.folio.service.event.handler.LocationUpdateEventHandler; import org.folio.service.event.handler.ServicePointDeleteEventHandler; import org.folio.service.event.handler.ServicePointUpdateEventHandler; @@ -83,6 +85,8 @@ private Future createConsumers() { new ServicePointUpdateEventHandler(context))) .compose(r -> createInventoryEventConsumer(INVENTORY_SERVICE_POINT_DELETED, config, new ServicePointDeleteEventHandler(context))) + .compose(r -> createInventoryEventConsumer(INVENTORY_LOCATION_UPDATED, config, + new LocationUpdateEventHandler(context))) .mapEmpty(); } diff --git a/src/main/java/org/folio/service/event/InventoryEventType.java b/src/main/java/org/folio/service/event/InventoryEventType.java index fddd0b305..69d0789e5 100644 --- a/src/main/java/org/folio/service/event/InventoryEventType.java +++ b/src/main/java/org/folio/service/event/InventoryEventType.java @@ -3,6 +3,7 @@ import static org.folio.service.event.InventoryEventType.PayloadType.DELETE; import static org.folio.service.event.InventoryEventType.PayloadType.UPDATE; import static org.folio.support.kafka.topic.InventoryKafkaTopic.ITEM; +import static org.folio.support.kafka.topic.InventoryKafkaTopic.LOCATION; import static org.folio.support.kafka.topic.InventoryKafkaTopic.SERVICE_POINT; import org.folio.kafka.services.KafkaTopic; @@ -15,7 +16,8 @@ public enum InventoryEventType { INVENTORY_ITEM_UPDATED(ITEM, UPDATE), INVENTORY_SERVICE_POINT_UPDATED(SERVICE_POINT, UPDATE), - INVENTORY_SERVICE_POINT_DELETED(SERVICE_POINT, DELETE); + INVENTORY_SERVICE_POINT_DELETED(SERVICE_POINT, DELETE), + INVENTORY_LOCATION_UPDATED(LOCATION, UPDATE); private final KafkaTopic kafkaTopic; private final PayloadType payloadType; diff --git a/src/main/java/org/folio/service/event/handler/LocationUpdateEventHandler.java b/src/main/java/org/folio/service/event/handler/LocationUpdateEventHandler.java new file mode 100644 index 000000000..935a8d07b --- /dev/null +++ b/src/main/java/org/folio/service/event/handler/LocationUpdateEventHandler.java @@ -0,0 +1,31 @@ +package org.folio.service.event.handler; + +import io.vertx.core.Context; +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import io.vertx.kafka.client.consumer.KafkaConsumerRecord; +import org.apache.commons.collections4.map.CaseInsensitiveMap; +import org.folio.kafka.AsyncRecordHandler; +import org.folio.persist.RequestRepository; +import org.folio.service.event.handler.processor.ItemLocationUpdateProcessorForRequest; + +import static org.folio.kafka.KafkaHeaderUtils.kafkaHeadersToMap; + +public class LocationUpdateEventHandler implements AsyncRecordHandler { + private final Context context; + public LocationUpdateEventHandler(Context context) { + this.context = context; + } + + @Override + public Future handle(KafkaConsumerRecord kafkaConsumerRecord) { + JsonObject payload = new JsonObject(kafkaConsumerRecord.value()); + CaseInsensitiveMap headers = + new CaseInsensitiveMap<>(kafkaHeadersToMap(kafkaConsumerRecord.headers())); + + ItemLocationUpdateProcessorForRequest itemLocationUpdateProcessorForRequest = + new ItemLocationUpdateProcessorForRequest(new RequestRepository(context, headers)); + + return itemLocationUpdateProcessorForRequest.run(kafkaConsumerRecord.key(), payload); + } +} diff --git a/src/main/java/org/folio/service/event/handler/processor/ItemLocationUpdateProcessorForRequest.java b/src/main/java/org/folio/service/event/handler/processor/ItemLocationUpdateProcessorForRequest.java new file mode 100644 index 000000000..0d8ac8dcc --- /dev/null +++ b/src/main/java/org/folio/service/event/handler/processor/ItemLocationUpdateProcessorForRequest.java @@ -0,0 +1,59 @@ +package org.folio.service.event.handler.processor; + +import io.vertx.core.Future; +import io.vertx.core.json.JsonObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.persist.RequestRepository; +import org.folio.rest.jaxrs.model.Request; +import org.folio.rest.persist.Criteria.Criteria; +import org.folio.rest.persist.Criteria.Criterion; + +import java.util.ArrayList; +import java.util.List; + +import static org.apache.commons.lang3.ObjectUtils.notEqual; +import static org.folio.service.event.InventoryEventType.INVENTORY_ITEM_UPDATED; +import static org.folio.service.event.handler.processor.ItemUpdateProcessorForRequest.ITEM_EFFECTIVE_LOCATION_ID; + +public class ItemLocationUpdateProcessorForRequest extends UpdateEventProcessor { + private static final Logger log = LogManager.getLogger(ItemLocationUpdateProcessorForRequest.class); + + private static final String LOCATION_NAME_KEY = "name"; + + + public ItemLocationUpdateProcessorForRequest(RequestRepository repository) { + super(INVENTORY_ITEM_UPDATED, repository); + } + + @Override + protected Future>> collectRelevantChanges(JsonObject payload) { + JsonObject oldObject = payload.getJsonObject("old"); + JsonObject newObject = payload.getJsonObject("new"); + + List> changes = new ArrayList<>(); + + // compare shelving order + String oldLocationName = oldObject.getString(LOCATION_NAME_KEY); + String newLocationName = newObject.getString(LOCATION_NAME_KEY); + if (notEqual(oldLocationName, newLocationName)) { + log.info("ItemLocationUpdateProcessorForRequest :: collectRelevantChanges:: changing item.itemEffectiveLocationId from {} to {}", + oldLocationName, newLocationName); + changes.add(new Change<>(request -> request.getItem().setItemEffectiveLocationName(newLocationName))); + } + + return Future.succeededFuture(changes); + } + + @Override + protected Criterion criterionForObjectsToBeUpdated(String oldObjectId) { + log.info("ItemLocationUpdateProcessorForRequest :: criterionForObjectsToBeUpdated:: oldObjectId: {}", + oldObjectId); + return new Criterion( + new Criteria() + .addField("'item'") + .addField(String.format("'%s'", ITEM_EFFECTIVE_LOCATION_ID)) + .setOperation("=") + .setVal(oldObjectId)); + } +} diff --git a/src/main/java/org/folio/support/kafka/topic/InventoryKafkaTopic.java b/src/main/java/org/folio/support/kafka/topic/InventoryKafkaTopic.java index 1e65d72fe..3ce76e841 100644 --- a/src/main/java/org/folio/support/kafka/topic/InventoryKafkaTopic.java +++ b/src/main/java/org/folio/support/kafka/topic/InventoryKafkaTopic.java @@ -4,7 +4,8 @@ public enum InventoryKafkaTopic implements KafkaTopic { ITEM("item", 10), - SERVICE_POINT("service-point", 10); + SERVICE_POINT("service-point", 10), + LOCATION("location", 10); private final String topic; private final int partitions; diff --git a/src/test/java/org/folio/rest/api/EventConsumerVerticleTest.java b/src/test/java/org/folio/rest/api/EventConsumerVerticleTest.java index f319fd12f..e7022c902 100644 --- a/src/test/java/org/folio/rest/api/EventConsumerVerticleTest.java +++ b/src/test/java/org/folio/rest/api/EventConsumerVerticleTest.java @@ -21,6 +21,7 @@ import static org.folio.rest.util.OkapiConnectionParams.OKAPI_TENANT_HEADER; import static org.folio.rest.util.OkapiConnectionParams.OKAPI_URL_HEADER; import static org.folio.service.event.InventoryEventType.INVENTORY_ITEM_UPDATED; +import static org.folio.service.event.InventoryEventType.INVENTORY_LOCATION_UPDATED; import static org.folio.service.event.InventoryEventType.INVENTORY_SERVICE_POINT_DELETED; import static org.folio.service.event.InventoryEventType.INVENTORY_SERVICE_POINT_UPDATED; import static org.folio.service.event.handler.processor.ItemUpdateProcessorForRequest.ITEM_EFFECTIVE_LOCATION_ID; @@ -96,6 +97,8 @@ public class EventConsumerVerticleTest extends ApiTests { "%s.%s.inventory.item", environment(), TENANT_ID); private static final String INVENTORY_SERVICE_POINT_TOPIC = format( "%s.%s.inventory.service-point", environment(), TENANT_ID); + private static final String INVENTORY_LOCATION_TOPIC = format( + "%s.%s.inventory.location", environment(), TENANT_ID); private static KafkaProducer producer; private static KafkaAdminClient adminClient; @@ -235,7 +238,6 @@ public void requestItemSpIsSetWhenItemEffectiveLocationPrimarySpChange() { //Change ServicePoint Name in stub and publish SP update event JsonObject servicePointNew = servicePoint.copy(); servicePointNew.put("name", "ServicePoint-2-Changed"); - createStubForServicePoints(List.of(servicePoint)); publishServicePointUpdateEvent(servicePoint, servicePointNew); // Expected RETRIEVAL_SERVICE_POINT in item @@ -247,6 +249,42 @@ public void requestItemSpIsSetWhenItemEffectiveLocationPrimarySpChange() { verifyRequestItemRetrievalServicePoint(REQUEST_ID, expectedItemRetrivalServicePoint); } + @Test + public void requestItemLocationIsSetWhenItemEffectiveLocationNameChange() { + //Creating and mocking ServicePoint + String servicePointId = UUID.randomUUID().toString(); + JsonObject servicePoint = buildServicePoint(servicePointId, "ServicePoint-1"); + createStubForServicePoints(List.of(servicePoint)); + + //Creating and mocking Location + String locationId = UUID.randomUUID().toString(); + JsonObject location = buildLocation(locationId, "Location-1", servicePointId); + createStubForLocations(List.of(location)); + + //Creating request with location in item object + JsonObject item = buildItem(); + JsonObject request = buildRequest(REQUEST_ID, item); + JsonObject requestItem = request.getJsonObject("item"); + requestItem.put(ITEM_EFFECTIVE_LOCATION_ID, locationId); + requestItem.put(ITEM_EFFECTIVE_LOCATION_NAME, location.getString("name")); + createRequest(request); + + int initialOffset = getOffsetForLocationUpdateEvents(); + + //Change Location Name in stub and publish location update event + JsonObject locationNew = location.copy(); + locationNew.put("name", "Location-2-Changed"); + publishLocationUpdateEvent(location, locationNew); + + // Expected LOCATION in item + JsonObject expectedItemLocation = new JsonObject(); + expectedItemLocation.put(ITEM_EFFECTIVE_LOCATION_ID, locationId); + expectedItemLocation.put(ITEM_EFFECTIVE_LOCATION_NAME, locationNew.getString("name")); + + waitUntilValueIsIncreased(initialOffset, EventConsumerVerticleTest::getOffsetForLocationUpdateEvents); + verifyRequestItemEffectiveLocation(REQUEST_ID, expectedItemLocation); + } + @Test public void requestSearchIndexIsNotUpdatedWhenRequestAndEventAreForDifferentItems() { JsonObject oldItem = buildItem(DEFAULT_CALL_NUMBER_PREFIX, DEFAULT_CALL_NUMBER, @@ -672,6 +710,10 @@ private void publishServicePointUpdateEvent(JsonObject oldServicePoint, publishEvent(INVENTORY_SERVICE_POINT_TOPIC, buildUpdateEvent(oldServicePoint, newServicePoint)); } + private void publishLocationUpdateEvent(JsonObject oldLocation, JsonObject newLocation) { + publishEvent(INVENTORY_LOCATION_TOPIC, buildUpdateEvent(oldLocation, newLocation)); + } + private void publishServicePointDeleteEvent(JsonObject servicePoint) { publishEvent(INVENTORY_SERVICE_POINT_TOPIC, buildDeleteEvent(servicePoint)); } @@ -754,6 +796,17 @@ private JsonObject verifyRequestItemRetrievalServicePoint(String requestId, Json }, equalTo(itemObject)); } + private JsonObject verifyRequestItemEffectiveLocation(String requestId, JsonObject itemObject) { + return waitAtMost(60, SECONDS) + .until(() -> { + JsonObject requestItem = getRequestItem(requestId); + JsonObject actualItem = new JsonObject(); + actualItem.put(ITEM_EFFECTIVE_LOCATION_ID, requestItem.getString(ITEM_EFFECTIVE_LOCATION_ID)); + actualItem.put(ITEM_EFFECTIVE_LOCATION_NAME, requestItem.getString(ITEM_EFFECTIVE_LOCATION_NAME)); + return actualItem; + }, equalTo(itemObject)); + } + private JsonObject verifyPickupServicePointName(String requestId, SearchIndex searchIndex) { return waitAtMost(60, SECONDS) .until(() -> getRequestSearchIndex(requestId), equalTo(mapFrom(searchIndex))); @@ -847,6 +900,11 @@ private static Integer getOffsetForServicePointUpdateEvents() { buildConsumerGroupId(INVENTORY_SERVICE_POINT_UPDATED.name())); } + private static Integer getOffsetForLocationUpdateEvents() { + return getOffset(INVENTORY_LOCATION_TOPIC, + buildConsumerGroupId(INVENTORY_LOCATION_UPDATED.name())); + } + private static Integer getOffsetForServicePointDeleteEvents() { return getOffset(INVENTORY_SERVICE_POINT_TOPIC, buildConsumerGroupId(INVENTORY_SERVICE_POINT_DELETED.name())); From ed1b13d1f33302d0db09ccf25c85febc38a436fb Mon Sep 17 00:00:00 2001 From: Kapil Verma Date: Mon, 13 Jan 2025 15:52:25 +0530 Subject: [PATCH 5/7] Update NEWS.md file --- NEWS.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.md b/NEWS.md index f3b7b8a40..ac61cdd2e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,9 @@ +## 17.3.3 2025-01-13 +* Add new LocationUpdateKafkaProcessor to sync location with request table (CIRCSTORE-556) +* Add new class like ServicePointUpdateProcessorForRequest to sync RetrievalSP updates in Request records (CIRCSTORE-541) +* Extend ItemUpdateProcessorForRequest to sync item updates in Request records (CIRCSTORE-540) +* Filter request by Retrieval Service Point (CIRCSTORE-539) + ## 17.3.2 2024-12-12 * Add Intermediate ECS request phase (CIRCSTORE-542) From 5f3bc885b3864b572bdcfb55655c36568a00c181 Mon Sep 17 00:00:00 2001 From: Kapil Verma Date: Mon, 13 Jan 2025 15:54:19 +0530 Subject: [PATCH 6/7] [maven-release-plugin] prepare release v17.3.3 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 981bec5b1..fef79a433 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 mod-circulation-storage org.folio - 17.3.3-SNAPSHOT + 17.3.3 Apache License 2.0 @@ -229,7 +229,7 @@ https://github.com/folio-org/mod-circulation-storage scm:git:git://github.com:folio-org/mod-circulation-storage.git scm:git:git@github.com:folio-org/mod-circulation-storage.git - v17.3.0 + v17.3.3 From 3493be23b573104ca950f246595ec3875ed73de7 Mon Sep 17 00:00:00 2001 From: Kapil Verma Date: Mon, 13 Jan 2025 15:54:19 +0530 Subject: [PATCH 7/7] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index fef79a433..6d1d9c070 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 mod-circulation-storage org.folio - 17.3.3 + 17.3.4-SNAPSHOT Apache License 2.0 @@ -229,7 +229,7 @@ https://github.com/folio-org/mod-circulation-storage scm:git:git://github.com:folio-org/mod-circulation-storage.git scm:git:git@github.com:folio-org/mod-circulation-storage.git - v17.3.3 + v17.3.0