From d2cfce1548634dd118e5f6213e9ee7491ad3af38 Mon Sep 17 00:00:00 2001 From: Abdulkhakimov <89521577+Abdulkhakimov@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:56:59 +0500 Subject: [PATCH] [MODORDERS-929] - Local shadow instance creation for linked instances (#776) --- descriptors/ModuleDescriptor-template.json | 44 +++++++++-- pom.xml | 5 -- .../org/folio/config/ApplicationConfig.java | 26 +++++-- .../folio/helper/PurchaseOrderLineHelper.java | 11 ++- .../consortium/ConsortiumConfiguration.java | 4 + .../models/consortium/SharingInstance.java | 18 +++++ .../models/consortium/SharingStatus.java | 38 +++++++++ .../orders/utils/ResourcePathResolver.java | 2 + .../core/exceptions/ConsortiumException.java | 10 +++ .../ConsortiumConfigurationService.java | 70 +++++++++++++++++ .../consortium/SharingInstanceService.java | 77 +++++++++++++++++++ .../service/inventory/InventoryManager.java | 32 +++++++- .../OrderLinePatchOperationService.java | 9 ++- .../folio/service/titles/TitlesService.java | 14 +++- src/test/java/org/folio/ApiTestSuite.java | 5 ++ .../java/org/folio/rest/impl/MockServer.java | 8 ++ .../SharingInstanceServiceTest.java | 74 ++++++++++++++++++ .../inventory/InventoryManagerTest.java | 19 ++++- .../OrderLineUpdateInstanceHandlerTest.java | 18 ++++- 19 files changed, 452 insertions(+), 32 deletions(-) create mode 100644 src/main/java/org/folio/models/consortium/ConsortiumConfiguration.java create mode 100644 src/main/java/org/folio/models/consortium/SharingInstance.java create mode 100644 src/main/java/org/folio/models/consortium/SharingStatus.java create mode 100644 src/main/java/org/folio/rest/core/exceptions/ConsortiumException.java create mode 100644 src/main/java/org/folio/service/consortium/ConsortiumConfigurationService.java create mode 100644 src/main/java/org/folio/service/consortium/SharingInstanceService.java create mode 100644 src/test/java/org/folio/service/consortium/SharingInstanceServiceTest.java diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 569c0d323..b2736e7a1 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -175,9 +175,12 @@ "configuration.entries.collection.get", "acquisitions-units-storage.units.collection.get", "acquisitions-units-storage.memberships.collection.get", + "inventory.instances.item.get", "inventory-storage.identifier-types.collection.get", "isbn-utils.convert-to-13.get", - "finance-storage.budget-expense-classes.collection.get" + "finance-storage.budget-expense-classes.collection.get", + "user-tenants.collection.get", + "consortia.sharing-instances.item.post" ] }, { @@ -235,6 +238,7 @@ "finance-storage.budget-expense-classes.collection.get", "inventory.instances.collection.get", "inventory.instances.item.post", + "inventory.instances.item.get", "inventory-storage.holdings.item.post", "inventory-storage.holdings.collection.get", "inventory-storage.items.collection.get", @@ -257,7 +261,9 @@ "organizations-storage.organizations.collection.get", "invoice.invoice-lines.collection.get", "invoice.invoice-lines.item.put", - "orders-storage.order-invoice-relationships.collection.get" + "orders-storage.order-invoice-relationships.collection.get", + "user-tenants.collection.get", + "consortia.sharing-instances.item.post" ] }, { @@ -309,7 +315,9 @@ "orders-storage.po-lines.item.patch", "orders-storage.po-lines.item.get", "orders-storage.po-lines.item.put", - "orders-storage.pieces.collection.get" + "orders-storage.pieces.collection.get", + "user-tenants.collection.get", + "consortia.sharing-instances.item.post" ] }, { @@ -465,6 +473,7 @@ "inventory.items.item.put", "inventory.items.collection.get", "inventory-storage.holdings-sources.collection.get", + "inventory.instances.item.get", "inventory.instances.item.post", "inventory-storage.holdings.collection.get", "inventory-storage.holdings.item.post", @@ -487,7 +496,9 @@ "orders-storage.titles.item.get", "orders-storage.titles.item.put", "orders-storage.alerts.item.get", - "orders-storage.reporting-codes.item.get" + "orders-storage.reporting-codes.item.get", + "user-tenants.collection.get", + "consortia.sharing-instances.item.post" ] }, { @@ -506,6 +517,7 @@ "acquisitions-units-storage.units.collection.get", "acquisitions-units-storage.memberships.collection.get", "configuration.entries.collection.get", + "inventory.instances.item.get", "inventory.instances.item.post", "inventory.items.item.get", "inventory.items.item.put", @@ -536,7 +548,9 @@ "orders-storage.titles.item.get", "orders-storage.titles.item.put", "orders-storage.alerts.item.get", - "orders-storage.reporting-codes.item.get" + "orders-storage.reporting-codes.item.get", + "user-tenants.collection.get", + "consortia.sharing-instances.item.post" ] }, { @@ -748,7 +762,10 @@ "modulePermissions": [ "orders-storage.titles.collection.get", "orders-storage.titles.item.post", - "orders-storage.po-lines.item.get" + "orders-storage.po-lines.item.get", + "inventory.instances.item.get", + "user-tenants.collection.get", + "consortia.sharing-instances.item.post" ] }, { @@ -761,7 +778,12 @@ "methods": ["PUT"], "pathPattern": "/orders/titles/{id}", "permissionsRequired": ["orders.titles.item.put"], - "modulePermissions": ["orders-storage.titles.item.put"] + "modulePermissions": [ + "orders-storage.titles.item.put", + "inventory.instances.item.get", + "user-tenants.collection.get", + "consortia.sharing-instances.item.post" + ] }, { "methods": ["DELETE"], @@ -1132,12 +1154,20 @@ { "id": "orders-storage.export-history", "version": "1.0" + }, + { + "id": "user-tenants", + "version": "1.0" } ], "optional": [ { "id": "invoice", "version": "7.0" + }, + { + "id": "consortia", + "version": "1.0" } ], "permissionSets": [ diff --git a/pom.xml b/pom.xml index 004c868e3..ef1f91b2c 100644 --- a/pom.xml +++ b/pom.xml @@ -170,11 +170,6 @@ 1.4.2 pom - - org.folio - folio-kafka-wrapper - 2.6.0 - org.folio data-import-processing-core diff --git a/src/main/java/org/folio/config/ApplicationConfig.java b/src/main/java/org/folio/config/ApplicationConfig.java index 33018cd17..9a0de7bb7 100644 --- a/src/main/java/org/folio/config/ApplicationConfig.java +++ b/src/main/java/org/folio/config/ApplicationConfig.java @@ -29,6 +29,8 @@ import org.folio.service.caches.ConfigurationEntriesCache; import org.folio.service.caches.InventoryCache; import org.folio.service.configuration.ConfigurationEntriesService; +import org.folio.service.consortium.ConsortiumConfigurationService; +import org.folio.service.consortium.SharingInstanceService; import org.folio.service.exchange.ExchangeRateProviderResolver; import org.folio.service.exchange.FinanceExchangeRateService; import org.folio.service.finance.FiscalYearService; @@ -451,8 +453,8 @@ CompositeOrderDynamicDataPopulateService combinedPopulateService(CompositeOrderR @Bean TitlesService titlesService(RestClient restClient, PurchaseOrderLineService purchaseOrderLineService, - AcquisitionsUnitsService acquisitionsUnitsService) { - return new TitlesService(restClient, purchaseOrderLineService, acquisitionsUnitsService); + AcquisitionsUnitsService acquisitionsUnitsService, InventoryManager inventoryManager) { + return new TitlesService(restClient, purchaseOrderLineService, acquisitionsUnitsService, inventoryManager); } @Bean @@ -477,8 +479,9 @@ ProtectionService protectionHelper(AcquisitionsUnitsService acquisitionsUnitsSer @Bean InventoryManager inventoryManager(RestClient restClient, ConfigurationEntriesCache configurationEntriesCache, - PieceStorageService pieceStorageService, InventoryCache inventoryCache, InventoryService inventoryService) { - return new InventoryManager(restClient, configurationEntriesCache, pieceStorageService, inventoryCache, inventoryService); + PieceStorageService pieceStorageService, InventoryCache inventoryCache, InventoryService inventoryService, + ConsortiumConfigurationService consortiumConfigurationService, SharingInstanceService sharingInstanceService) { + return new InventoryManager(restClient, configurationEntriesCache, pieceStorageService, inventoryCache, inventoryService, sharingInstanceService, consortiumConfigurationService); } @Bean @@ -682,8 +685,9 @@ PurchaseOrderHelper purchaseOrderHelper(PurchaseOrderLineHelper purchaseOrderLin RestClient restClient, OrderLinePatchOperationHandlerResolver orderLinePatchOperationHandlerResolver, PurchaseOrderLineService purchaseOrderLineService, - InventoryCache inventoryCache) { - return new OrderLinePatchOperationService(restClient, orderLinePatchOperationHandlerResolver, purchaseOrderLineService, inventoryCache); + InventoryCache inventoryCache, + InventoryManager inventoryManager) { + return new OrderLinePatchOperationService(restClient, orderLinePatchOperationHandlerResolver, purchaseOrderLineService, inventoryCache, inventoryManager); } @Bean PatchOperationHandler orderLineUpdateInstanceHandler( @@ -733,4 +737,14 @@ OrderTemplatesService orderTemplatesService() { @Bean POLInvoiceLineRelationService polInvoiceLineRelationService(InvoiceLineService invoiceLineService, PendingPaymentService pendingPaymentService, InvoiceTransactionSummariesService invoiceTransactionSummariesService, PoLineInvoiceLineHolderBuilder poLineInvoiceLineHolderBuilder) { return new POLInvoiceLineRelationService(invoiceLineService, pendingPaymentService, invoiceTransactionSummariesService, poLineInvoiceLineHolderBuilder); } + + @Bean + ConsortiumConfigurationService consortiumConfigurationService(RestClient restClient) { + return new ConsortiumConfigurationService(restClient); + } + + @Bean + SharingInstanceService sharingInstanceService(RestClient restClient) { + return new SharingInstanceService(restClient); + } } diff --git a/src/main/java/org/folio/helper/PurchaseOrderLineHelper.java b/src/main/java/org/folio/helper/PurchaseOrderLineHelper.java index 7d7e52c40..d6fe18ae7 100644 --- a/src/main/java/org/folio/helper/PurchaseOrderLineHelper.java +++ b/src/main/java/org/folio/helper/PurchaseOrderLineHelper.java @@ -187,6 +187,7 @@ public Future createPoLine(CompositePoLine compPOL, JsonObject // The PO Line can be created only for order in Pending state .map(this::validateOrderState) .compose(po -> protectionService.isOperationRestricted(po.getAcqUnitIds(), ProtectedOperationType.CREATE, requestContext) + .compose(v -> createShadowInstanceIfNeeded(compPOL, requestContext)) .compose(v -> createPoLine(compPOL, po, requestContext))); } else { Errors errors = new Errors().withErrors(validationErrors).withTotalRecords(validationErrors.size()); @@ -297,6 +298,7 @@ public Future updateOrderLine(CompositePoLine compOrderLine, RequestContex compOrderLine.setPoLineNumber(lineFromStorage.getString(PO_LINE_NUMBER)); return polInvoiceLineRelationService.prepareRelatedInvoiceLines(poLineInvoiceLineHolder, requestContext) + .compose(v -> createShadowInstanceIfNeeded(compOrderLine, requestContext)) .compose(v -> updateOrderLine(compOrderLine, lineFromStorage, requestContext)) .compose(v -> updateEncumbranceStatus(compOrderLine, lineFromStorage, requestContext)) .compose(v -> polInvoiceLineRelationService.updateInvoiceLineReference(poLineInvoiceLineHolder, requestContext)) @@ -884,6 +886,13 @@ private Future verifyDeleteAllowed(PoLine line, RequestContext requestCont .compose(order -> protectionService.isOperationRestricted(order.getAcqUnitIds(), DELETE, requestContext))); } - + private Future createShadowInstanceIfNeeded(CompositePoLine compositePoLine, RequestContext requestContext) { + String instanceId = compositePoLine.getInstanceId(); + if (Boolean.TRUE.equals(compositePoLine.getIsPackage()) || Objects.isNull(instanceId)) { + return Future.succeededFuture(); + } + return inventoryManager.createShadowInstanceIfNeeded(instanceId, requestContext) + .mapEmpty(); + } } diff --git a/src/main/java/org/folio/models/consortium/ConsortiumConfiguration.java b/src/main/java/org/folio/models/consortium/ConsortiumConfiguration.java new file mode 100644 index 000000000..b1152f784 --- /dev/null +++ b/src/main/java/org/folio/models/consortium/ConsortiumConfiguration.java @@ -0,0 +1,4 @@ +package org.folio.models.consortium; + +public record ConsortiumConfiguration(String centralTenantId, String consortiumId) { +} diff --git a/src/main/java/org/folio/models/consortium/SharingInstance.java b/src/main/java/org/folio/models/consortium/SharingInstance.java new file mode 100644 index 000000000..7b67fbb3a --- /dev/null +++ b/src/main/java/org/folio/models/consortium/SharingInstance.java @@ -0,0 +1,18 @@ +package org.folio.models.consortium; + + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.UUID; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record SharingInstance(UUID id, + UUID instanceIdentifier, + String sourceTenantId, + String targetTenantId, + SharingStatus status, + String error) { + public SharingInstance(UUID instanceIdentifier, String sourceTenantId, String targetTenantId) { + this(null, instanceIdentifier, sourceTenantId, targetTenantId, null, null); + } +} diff --git a/src/main/java/org/folio/models/consortium/SharingStatus.java b/src/main/java/org/folio/models/consortium/SharingStatus.java new file mode 100644 index 000000000..d1047e1ba --- /dev/null +++ b/src/main/java/org/folio/models/consortium/SharingStatus.java @@ -0,0 +1,38 @@ +package org.folio.models.consortium; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum SharingStatus { + COMPLETE("COMPLETE"), + + ERROR("ERROR"), + + IN_PROGRESS("IN_PROGRESS"); + + private final String value; + + SharingStatus(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static SharingStatus fromValue(String value) { + for (SharingStatus b : SharingStatus.values()) { + if (b.value.equals(value)) { + return b; + } + } + throw new IllegalArgumentException("Unexpected value '" + value + "'"); + } +} diff --git a/src/main/java/org/folio/orders/utils/ResourcePathResolver.java b/src/main/java/org/folio/orders/utils/ResourcePathResolver.java index 59503beb4..1c29369fa 100644 --- a/src/main/java/org/folio/orders/utils/ResourcePathResolver.java +++ b/src/main/java/org/folio/orders/utils/ResourcePathResolver.java @@ -37,6 +37,7 @@ private ResourcePathResolver() { public static final String PREFIXES = "configuration.prefixes"; public static final String SUFFIXES = "configuration.suffixes"; public static final String TRANSACTIONS_ENDPOINT = "finance.transactions"; + public static final String USER_TENANTS_ENDPOINT = "user.tenants"; public static final String FINANCE_RELEASE_ENCUMBRANCE = "finance.release-encumbrance"; public static final String BUDGET_EXPENSE_CLASSES = "finance-storage.budget-expense-classes"; public static final String CURRENT_BUDGET = "finance.current-budgets"; @@ -77,6 +78,7 @@ private ResourcePathResolver() { apis.put(PREFIXES, "/orders-storage/configuration/prefixes"); apis.put(SUFFIXES, "/orders-storage/configuration/suffixes"); apis.put(TRANSACTIONS_ENDPOINT, "/finance/transactions"); + apis.put(USER_TENANTS_ENDPOINT, "/user-tenants"); apis.put(FINANCE_RELEASE_ENCUMBRANCE, "/finance/release-encumbrance"); apis.put(BUDGET_EXPENSE_CLASSES, "/finance-storage/budget-expense-classes"); apis.put(CURRENT_BUDGET, "/finance/funds/%s/budget"); diff --git a/src/main/java/org/folio/rest/core/exceptions/ConsortiumException.java b/src/main/java/org/folio/rest/core/exceptions/ConsortiumException.java new file mode 100644 index 000000000..55095a967 --- /dev/null +++ b/src/main/java/org/folio/rest/core/exceptions/ConsortiumException.java @@ -0,0 +1,10 @@ +package org.folio.rest.core.exceptions; + +/** + * Exception that used for consortium process + */ +public class ConsortiumException extends RuntimeException { + public ConsortiumException(String message) { + super(message); + } +} diff --git a/src/main/java/org/folio/service/consortium/ConsortiumConfigurationService.java b/src/main/java/org/folio/service/consortium/ConsortiumConfigurationService.java new file mode 100644 index 000000000..85a9d80b1 --- /dev/null +++ b/src/main/java/org/folio/service/consortium/ConsortiumConfigurationService.java @@ -0,0 +1,70 @@ +package org.folio.service.consortium; + +import com.github.benmanes.caffeine.cache.AsyncCache; +import com.github.benmanes.caffeine.cache.Caffeine; +import io.vertx.core.Future; +import io.vertx.core.Vertx; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.models.consortium.ConsortiumConfiguration; +import org.folio.rest.core.RestClient; +import org.folio.rest.core.models.RequestContext; +import org.folio.rest.core.models.RequestEntry; +import org.folio.rest.tools.utils.TenantTool; +import org.springframework.beans.factory.annotation.Value; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +public class ConsortiumConfigurationService { + private static final Logger logger = LogManager.getLogger(ConsortiumConfigurationService.class); + + private static final String CONSORTIUM_ID_FIELD = "consortiumId"; + private static final String CENTRAL_TENANT_ID_FIELD = "centralTenantId"; + private static final String USER_TENANTS_ARRAY_IDENTIFIER = "userTenants"; + private static final String USER_TENANTS_ENDPOINT = "/user-tenants"; + + @Value("${orders.cache.consortium-data.expiration.time.seconds:300}") + private long cacheExpirationTime; + + private final RestClient restClient; + private final AsyncCache> asyncCache; + + public ConsortiumConfigurationService(RestClient restClient) { + this.restClient = restClient; + + asyncCache = Caffeine.newBuilder() + .expireAfterWrite(cacheExpirationTime, TimeUnit.SECONDS) + .executor(task -> Vertx.currentContext().runOnContext(v -> task.run())) + .buildAsync(); + } + + public Future> getConsortiumConfiguration(RequestContext requestContext) { + try { + var cacheKey = TenantTool.tenantId(requestContext.getHeaders()); + return Future.fromCompletionStage(asyncCache.get(cacheKey, (key, executor) -> + getConsortiumConfigurationFromRemote(requestContext))); + } catch (Exception e) { + logger.error("Error when retrieving consortium configuration", e); + return Future.failedFuture(e); + } + } + + private CompletableFuture> getConsortiumConfigurationFromRemote(RequestContext requestContext) { + RequestEntry requestEntry = new RequestEntry(USER_TENANTS_ENDPOINT).withLimit(1); + return restClient.getAsJsonObject(requestEntry, requestContext) + .map(jsonObject -> jsonObject.getJsonArray(USER_TENANTS_ARRAY_IDENTIFIER)) + .map(userTenants -> { + if (userTenants.isEmpty()) { + logger.debug("Central tenant and consortium id not found"); + return Optional.empty(); + } + String consortiumId = userTenants.getJsonObject(0).getString(CONSORTIUM_ID_FIELD); + String centralTenantId = userTenants.getJsonObject(0).getString(CENTRAL_TENANT_ID_FIELD); + logger.debug("Found centralTenantId: {} and consortiumId: {}", centralTenantId, consortiumId); + return Optional.of(new ConsortiumConfiguration(centralTenantId, consortiumId)); + }).toCompletionStage().toCompletableFuture(); + } + +} diff --git a/src/main/java/org/folio/service/consortium/SharingInstanceService.java b/src/main/java/org/folio/service/consortium/SharingInstanceService.java new file mode 100644 index 000000000..54fa9ff07 --- /dev/null +++ b/src/main/java/org/folio/service/consortium/SharingInstanceService.java @@ -0,0 +1,77 @@ +package org.folio.service.consortium; + +import io.vertx.core.Context; +import io.vertx.core.Future; +import org.apache.commons.collections4.map.CaseInsensitiveMap; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.folio.models.consortium.ConsortiumConfiguration; +import org.folio.models.consortium.SharingInstance; +import org.folio.models.consortium.SharingStatus; +import org.folio.okapi.common.XOkapiHeaders; +import org.folio.rest.core.RestClient; +import org.folio.rest.core.exceptions.ConsortiumException; +import org.folio.rest.core.models.RequestContext; +import org.folio.rest.core.models.RequestEntry; +import org.folio.rest.tools.utils.TenantTool; + +import java.util.Map; +import java.util.UUID; + +/** + * The `SharingInstanceService` class manages the creation of shadow instances + * within a consortium using the `mod-consortia` module. + * It provides methods for creating shadow instances and sharing them within the consortium. + * This service is responsible for making REST API calls to the `mod-consortia` module. + */ +public class SharingInstanceService { + private static final Logger logger = LogManager.getLogger(SharingInstanceService.class); + + private static final String SHARE_INSTANCE_ENDPOINT = "/consortia/{id}/sharing/instances"; + private static final String SHARING_INSTANCE_ERROR = "Error during sharing Instance for sourceTenantId: %s, targetTenantId: %s, instanceIdentifier: %s, error: %s"; + + private final RestClient restClient; + + public SharingInstanceService(RestClient restClient) { + this.restClient = restClient; + } + + /** + * Creates a shadow instance and shares it within the consortium. + * + * @param instanceId the unique identifier of the instance to be shared + * @param consortiumConfiguration the consortium configuration + * @param requestContext the request context + * @return a Future that resolves with the created SharingInstance + */ + public Future createShadowInstance(String instanceId, ConsortiumConfiguration consortiumConfiguration, RequestContext requestContext) { + SharingInstance sharingInstance = new SharingInstance(UUID.fromString(instanceId), + consortiumConfiguration.centralTenantId(), TenantTool.tenantId(requestContext.getHeaders())); + RequestContext consortiaRequestContext = createRequestContextWithUpdatedTenantId(requestContext.getContext(), + requestContext.getHeaders(), consortiumConfiguration.centralTenantId()); + return shareInstance(consortiumConfiguration.consortiumId(), sharingInstance, consortiaRequestContext); + } + + private Future shareInstance(String consortiumId, SharingInstance sharingInstance, RequestContext requestContext) { + RequestEntry requestEntry = new RequestEntry(SHARE_INSTANCE_ENDPOINT).withId(consortiumId); + return restClient.post(requestEntry, sharingInstance, SharingInstance.class, requestContext) + .compose(response -> { + if (ObjectUtils.notEqual(SharingStatus.ERROR, response.status())) { + logger.debug("Successfully sharedInstance with id: {}, sharedInstance: {}", response.instanceIdentifier(), response); + return Future.succeededFuture(response); + } else { + String message = String.format(SHARING_INSTANCE_ERROR, sharingInstance.sourceTenantId(), sharingInstance.targetTenantId(), + sharingInstance.instanceIdentifier(), sharingInstance.error()); + return Future.failedFuture(new ConsortiumException(message)); + } + }); + } + + private RequestContext createRequestContextWithUpdatedTenantId(Context context, Map headers, String centralTenantId) { + Map modifiedHeaders = new CaseInsensitiveMap<>(headers); + modifiedHeaders.put(XOkapiHeaders.TENANT, centralTenantId); + return new RequestContext(context, modifiedHeaders ); + } + +} diff --git a/src/main/java/org/folio/service/inventory/InventoryManager.java b/src/main/java/org/folio/service/inventory/InventoryManager.java index 5a817b03d..124d0e10e 100644 --- a/src/main/java/org/folio/service/inventory/InventoryManager.java +++ b/src/main/java/org/folio/service/inventory/InventoryManager.java @@ -42,6 +42,7 @@ import org.apache.logging.log4j.Logger; import org.folio.models.PieceItemPair; import org.folio.models.PoLineUpdateHolder; +import org.folio.models.consortium.SharingInstance; import org.folio.okapi.common.GenericCompositeFuture; import org.folio.orders.utils.HelperUtils; import org.folio.orders.utils.PoLineCommonUtil; @@ -65,6 +66,8 @@ import org.folio.rest.tools.utils.TenantTool; import org.folio.service.caches.ConfigurationEntriesCache; import org.folio.service.caches.InventoryCache; +import org.folio.service.consortium.ConsortiumConfigurationService; +import org.folio.service.consortium.SharingInstanceService; import org.folio.service.pieces.PieceStorageService; import io.vertx.core.CompositeFuture; @@ -150,14 +153,18 @@ public class InventoryManager { private final InventoryCache inventoryCache; private final InventoryService inventoryService; private final PieceStorageService pieceStorageService; + private final SharingInstanceService sharingInstanceService; + private final ConsortiumConfigurationService consortiumConfigurationService; public InventoryManager(RestClient restClient, ConfigurationEntriesCache configurationEntriesCache, - PieceStorageService pieceStorageService, InventoryCache inventoryCache, InventoryService inventoryService) { + PieceStorageService pieceStorageService, InventoryCache inventoryCache, InventoryService inventoryService, SharingInstanceService sharingInstanceService, ConsortiumConfigurationService consortiumConfigurationService) { this.restClient = restClient; this.configurationEntriesCache = configurationEntriesCache; this.inventoryCache = inventoryCache; this.inventoryService = inventoryService; this.pieceStorageService = pieceStorageService; + this.sharingInstanceService = sharingInstanceService; + this.consortiumConfigurationService = consortiumConfigurationService; } static { @@ -1019,6 +1026,11 @@ public Future createInstance(JsonObject instanceRecJson, RequestContext return restClient.postJsonObjectAndGetId(requestEntry, instanceRecJson, requestContext); } + public Future getInstanceById(String instanceId, boolean skipNotFoundException, RequestContext requestContext) { + RequestEntry requestEntry = new RequestEntry(INVENTORY_LOOKUP_ENDPOINTS.get(INSTANCE_RECORDS_BY_ID_ENDPOINT)).withId(instanceId); + return restClient.getAsJsonObject(requestEntry, skipNotFoundException, requestContext); + } + public Future deleteHoldingById(String holdingId, boolean skipNotFoundException, RequestContext requestContext) { if (StringUtils.isNotEmpty(holdingId)) { RequestEntry requestEntry = new RequestEntry(INVENTORY_LOOKUP_ENDPOINTS.get(HOLDINGS_RECORDS_BY_ID_ENDPOINT)) @@ -1265,4 +1277,22 @@ private void updateItemWithPieceFields(Piece piece, JsonObject item) { Optional.ofNullable(piece.getDiscoverySuppress()) .ifPresentOrElse(discSup -> item.put(ITEM_DISCOVERY_SUPPRESS, discSup), () -> item.remove(ITEM_DISCOVERY_SUPPRESS)); } + + public Future createShadowInstanceIfNeeded(String instanceId, RequestContext requestContext) { + return consortiumConfigurationService.getConsortiumConfiguration(requestContext) + .compose(consortiumConfiguration -> { + if (consortiumConfiguration.isPresent()) { + return getInstanceById(instanceId, true, requestContext) + .compose(instance -> { + if (Objects.nonNull(instance) && !instance.isEmpty()) { + return Future.succeededFuture(); + } + logger.info("Creating shadow instance with instanceId: {}", instanceId); + return sharingInstanceService.createShadowInstance(instanceId, consortiumConfiguration.get(), requestContext); + }); + } + return Future.succeededFuture(); + }); + } + } diff --git a/src/main/java/org/folio/service/orders/lines/update/OrderLinePatchOperationService.java b/src/main/java/org/folio/service/orders/lines/update/OrderLinePatchOperationService.java index 6857374d4..c00ac9655 100644 --- a/src/main/java/org/folio/service/orders/lines/update/OrderLinePatchOperationService.java +++ b/src/main/java/org/folio/service/orders/lines/update/OrderLinePatchOperationService.java @@ -41,6 +41,7 @@ import org.folio.rest.jaxrs.model.PoLine; import org.folio.rest.jaxrs.model.ProductId; import org.folio.service.caches.InventoryCache; +import org.folio.service.inventory.InventoryManager; import org.folio.service.orders.PurchaseOrderLineService; import io.vertx.core.Future; @@ -63,17 +64,21 @@ public class OrderLinePatchOperationService { private final PurchaseOrderLineService purchaseOrderLineService; private final InventoryCache inventoryCache; + private final InventoryManager inventoryManager; public OrderLinePatchOperationService(RestClient restClient, OrderLinePatchOperationHandlerResolver orderLinePatchOperationHandlerResolver, - PurchaseOrderLineService purchaseOrderLineService, InventoryCache inventoryCache) { + PurchaseOrderLineService purchaseOrderLineService, InventoryCache inventoryCache, InventoryManager inventoryManager) { this.restClient = restClient; this.orderLinePatchOperationHandlerResolver = orderLinePatchOperationHandlerResolver; this.purchaseOrderLineService = purchaseOrderLineService; this.inventoryCache = inventoryCache; + this.inventoryManager = inventoryManager; } public Future patch(String lineId, PatchOrderLineRequest request, RequestContext requestContext) { - return patchOrderLine(request, lineId, requestContext) + String newInstanceId = request.getReplaceInstanceRef().getNewInstanceId(); + return inventoryManager.createShadowInstanceIfNeeded(newInstanceId, requestContext) + .compose(v -> patchOrderLine(request, lineId, requestContext)) .compose(v -> updateInventoryInstanceInformation(request, lineId, requestContext)); } diff --git a/src/main/java/org/folio/service/titles/TitlesService.java b/src/main/java/org/folio/service/titles/TitlesService.java index 40375776f..d81e1b55b 100644 --- a/src/main/java/org/folio/service/titles/TitlesService.java +++ b/src/main/java/org/folio/service/titles/TitlesService.java @@ -27,6 +27,7 @@ import org.folio.rest.jaxrs.model.Title; import org.folio.rest.jaxrs.model.TitleCollection; import org.folio.service.AcquisitionsUnitsService; +import org.folio.service.inventory.InventoryManager; import org.folio.service.orders.PurchaseOrderLineService; import io.vertx.core.Future; @@ -43,16 +44,19 @@ public class TitlesService { private final RestClient restClient; private final AcquisitionsUnitsService acquisitionsUnitsService; + private final InventoryManager inventoryManager; public TitlesService(RestClient restClient, PurchaseOrderLineService purchaseOrderLineService, - AcquisitionsUnitsService acquisitionsUnitsService) { + AcquisitionsUnitsService acquisitionsUnitsService, InventoryManager inventoryManager) { this.restClient = restClient; this.purchaseOrderLineService = purchaseOrderLineService; this.acquisitionsUnitsService = acquisitionsUnitsService; + this.inventoryManager = inventoryManager; } public Future createTitle(Title title, RequestContext requestContext) { - return populateTitle(title, title.getPoLineId(), requestContext) + return inventoryManager.createShadowInstanceIfNeeded(title.getInstanceId(), requestContext) + .compose(shadowInstance -> populateTitle(title, title.getPoLineId(), requestContext)) .compose(v -> { RequestEntry requestEntry = new RequestEntry(ENDPOINT); return restClient.post(requestEntry, title, Title.class, requestContext); @@ -80,8 +84,10 @@ public Future<Title> getTitleById(String titleId, RequestContext requestContext) } public Future<Void> saveTitle(Title title, RequestContext requestContext) { - RequestEntry requestEntry = new RequestEntry(BY_ID_ENDPOINT).withId(title.getId()); - return restClient.put(requestEntry, title, requestContext); + return inventoryManager.createShadowInstanceIfNeeded(title.getInstanceId(), requestContext).compose(shadowInstance -> { + RequestEntry requestEntry = new RequestEntry(BY_ID_ENDPOINT).withId(title.getId()); + return restClient.put(requestEntry, title, requestContext); + }); } public Future<Void> deleteTitle(String id, RequestContext requestContext) { diff --git a/src/test/java/org/folio/ApiTestSuite.java b/src/test/java/org/folio/ApiTestSuite.java index 2c26a6c05..183e9b2b0 100644 --- a/src/test/java/org/folio/ApiTestSuite.java +++ b/src/test/java/org/folio/ApiTestSuite.java @@ -42,6 +42,7 @@ import org.folio.service.PrefixServiceTest; import org.folio.service.ReasonForClosureServiceTest; import org.folio.service.SuffixServiceTest; +import org.folio.service.consortium.SharingInstanceServiceTest; import org.folio.service.exchange.ManualExchangeRateProviderTest; import org.folio.service.expenceclass.ExpenseClassValidationServiceTest; import org.folio.service.finance.FundServiceTest; @@ -192,6 +193,10 @@ class SuffixServiceTestNested extends SuffixServiceTest { class PrefixServiceTestNested extends PrefixServiceTest { } + @Nested + class SharingInstanceServiceTestNested extends SharingInstanceServiceTest { + } + @Nested class ReasonForClosureServiceTestNested extends ReasonForClosureServiceTest { } diff --git a/src/test/java/org/folio/rest/impl/MockServer.java b/src/test/java/org/folio/rest/impl/MockServer.java index 3dac5d5aa..0c208ba09 100644 --- a/src/test/java/org/folio/rest/impl/MockServer.java +++ b/src/test/java/org/folio/rest/impl/MockServer.java @@ -63,6 +63,7 @@ import static org.folio.orders.utils.ResourcePathResolver.FINANCE_RELEASE_ENCUMBRANCE; import static org.folio.orders.utils.ResourcePathResolver.FUNDS; import static org.folio.orders.utils.ResourcePathResolver.LEDGERS; +import static org.folio.orders.utils.ResourcePathResolver.USER_TENANTS_ENDPOINT; import static org.folio.orders.utils.ResourcePathResolver.LEDGER_FY_ROLLOVERS; import static org.folio.orders.utils.ResourcePathResolver.LEDGER_FY_ROLLOVER_ERRORS; import static org.folio.orders.utils.ResourcePathResolver.ORDER_INVOICE_RELATIONSHIP; @@ -604,6 +605,7 @@ private Router defineRoutes() { router.get(resourcePath(PREFIXES)).handler(ctx -> handleGetGenericSubObj(ctx, PREFIXES)); router.get(resourcePath(SUFFIXES)).handler(ctx -> handleGetGenericSubObj(ctx, SUFFIXES)); router.get(resourcesPath(TRANSACTIONS_ENDPOINT)).handler(this::handleTransactionGetEntry); + router.get(resourcesPath(USER_TENANTS_ENDPOINT)).handler(this::handleUserTenantsGetEntry); router.get("/finance/funds/:id/budget").handler(this::handleGetBudgetByFinanceId); router.get(resourcesPath(FINANCE_EXCHANGE_RATE)).handler(this::handleGetRateOfExchange); router.get(resourcesPath(LEDGER_FY_ROLLOVERS)).handler(this::handleGetFyRollovers); @@ -2294,6 +2296,12 @@ private void handleTransactionGetEntry(RoutingContext ctx) { } } + private void handleUserTenantsGetEntry(RoutingContext ctx) { + String body = new JsonObject().put("userTenants", org.assertj.core.util.Lists.emptyList()).encodePrettily(); + serverResponse(ctx, HttpStatus.HTTP_OK.toInt(), APPLICATION_JSON, body); + addServerRqRsData(HttpMethod.GET, USER_TENANTS_ENDPOINT, new JsonObject(body)); + } + private Class<?> getSubObjClass(String subObj) { return switch (subObj) { case ALERTS -> Alert.class; diff --git a/src/test/java/org/folio/service/consortium/SharingInstanceServiceTest.java b/src/test/java/org/folio/service/consortium/SharingInstanceServiceTest.java new file mode 100644 index 000000000..7431c83d5 --- /dev/null +++ b/src/test/java/org/folio/service/consortium/SharingInstanceServiceTest.java @@ -0,0 +1,74 @@ +package org.folio.service.consortium; + +import io.vertx.core.Future; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import org.apache.commons.lang3.StringUtils; +import org.folio.models.consortium.ConsortiumConfiguration; +import org.folio.models.consortium.SharingInstance; +import org.folio.models.consortium.SharingStatus; +import org.folio.rest.core.RestClient; +import org.folio.rest.core.exceptions.ConsortiumException; +import org.folio.rest.core.models.RequestContext; +import org.folio.rest.core.models.RequestEntry; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; + +@ExtendWith({VertxExtension.class, MockitoExtension.class}) +public class SharingInstanceServiceTest { + + @InjectMocks + private SharingInstanceService sharingInstanceService; + + @Mock + private RestClient restClient; + + @Mock + private RequestContext requestContext; + + @Test + void testCreateShadowInstance(VertxTestContext vertxTestContext) { + String instanceId = UUID.randomUUID().toString(); + ConsortiumConfiguration consortiumConfiguration = new ConsortiumConfiguration("diku", "consortium"); + + Mockito.when(restClient.post(any(RequestEntry.class), any(), any(), any())) + .thenReturn(Future.succeededFuture(new SharingInstance(UUID.randomUUID(), StringUtils.EMPTY, StringUtils.EMPTY))); + + Future<SharingInstance> future = sharingInstanceService.createShadowInstance(instanceId, consortiumConfiguration, requestContext); + vertxTestContext.assertComplete(future) + .onComplete(ar -> { + Assertions.assertTrue(ar.succeeded()); + verify(restClient).post(any(RequestEntry.class), any(), any(), any()); + vertxTestContext.completeNow(); + }); + } + + @Test + void testCreateShadowInstanceFailure(VertxTestContext vertxTestContext) { + SharingInstanceService service = new SharingInstanceService(restClient); + String instanceId = UUID.randomUUID().toString(); + SharingInstance response = new SharingInstance(UUID.randomUUID(), UUID.randomUUID(), "test", "consortium", SharingStatus.ERROR, StringUtils.EMPTY); + ConsortiumConfiguration consortiumConfiguration = new ConsortiumConfiguration("diku", "consortium"); + + Mockito.when(restClient.post(any(RequestEntry.class), any(), any(), any())).thenReturn(Future.succeededFuture(response)); + + Future<SharingInstance> future = service.createShadowInstance(instanceId, consortiumConfiguration, requestContext); + vertxTestContext.assertFailure(future) + .onComplete(completionException -> { + assertEquals(ConsortiumException.class, completionException.cause().getClass()); + vertxTestContext.completeNow(); + }); + } + +} diff --git a/src/test/java/org/folio/service/inventory/InventoryManagerTest.java b/src/test/java/org/folio/service/inventory/InventoryManagerTest.java index b88c7a65b..4f3f3e2c2 100644 --- a/src/test/java/org/folio/service/inventory/InventoryManagerTest.java +++ b/src/test/java/org/folio/service/inventory/InventoryManagerTest.java @@ -86,6 +86,8 @@ import org.folio.service.caches.ConfigurationEntriesCache; import org.folio.service.caches.InventoryCache; import org.folio.service.configuration.ConfigurationEntriesService; +import org.folio.service.consortium.ConsortiumConfigurationService; +import org.folio.service.consortium.SharingInstanceService; import org.folio.service.pieces.PieceService; import org.folio.service.pieces.PieceStorageService; import org.hamcrest.core.IsInstanceOf; @@ -140,6 +142,10 @@ public class InventoryManagerTest { private InventoryCache inventoryCache; @Autowired private InventoryService inventoryService; + @Autowired + private SharingInstanceService sharingInstanceService; + @Autowired + private ConsortiumConfigurationService consortiumConfigurationService; private Map<String, String> okapiHeadersMock; @@ -1047,6 +1053,14 @@ public InventoryService inventoryService() { public PieceStorageService pieceStorageService() { return mock(PieceStorageService.class); } + @Bean + public ConsortiumConfigurationService consortiumConfigurationService() { + return mock(ConsortiumConfigurationService.class); + } + @Bean + public SharingInstanceService sharingInstanceService() { + return mock(SharingInstanceService.class); + } @Bean public RestClient restClient() { @@ -1055,8 +1069,9 @@ public RestClient restClient() { @Bean public InventoryManager inventoryManager(RestClient restClient, ConfigurationEntriesCache configurationEntriesCache, - PieceStorageService pieceStorageService, InventoryCache inventoryCache, InventoryService inventoryService) { - return spy(new InventoryManager(restClient, configurationEntriesCache, pieceStorageService, inventoryCache, inventoryService)); + PieceStorageService pieceStorageService, InventoryCache inventoryCache, InventoryService inventoryService, + ConsortiumConfigurationService consortiumConfigurationService, SharingInstanceService sharingInstanceService) { + return spy(new InventoryManager(restClient, configurationEntriesCache, pieceStorageService, inventoryCache, inventoryService, sharingInstanceService, consortiumConfigurationService)); } } } diff --git a/src/test/java/org/folio/service/orders/lines/update/OrderLineUpdateInstanceHandlerTest.java b/src/test/java/org/folio/service/orders/lines/update/OrderLineUpdateInstanceHandlerTest.java index 955c5c51d..161cdf309 100644 --- a/src/test/java/org/folio/service/orders/lines/update/OrderLineUpdateInstanceHandlerTest.java +++ b/src/test/java/org/folio/service/orders/lines/update/OrderLineUpdateInstanceHandlerTest.java @@ -36,6 +36,8 @@ import org.folio.service.caches.ConfigurationEntriesCache; import org.folio.service.caches.InventoryCache; import org.folio.service.configuration.ConfigurationEntriesService; +import org.folio.service.consortium.ConsortiumConfigurationService; +import org.folio.service.consortium.SharingInstanceService; import org.folio.service.inventory.InventoryManager; import org.folio.service.inventory.InventoryService; import org.folio.service.orders.PurchaseOrderLineService; @@ -212,6 +214,12 @@ static class ContextConfiguration { @Bean InventoryService inventoryService (RestClient restClient) { return new InventoryService(restClient); } + @Bean SharingInstanceService sharingInstanceService (RestClient restClient) { + return new SharingInstanceService(restClient); + } + @Bean ConsortiumConfigurationService consortiumConfigurationService (RestClient restClient) { + return new ConsortiumConfigurationService(restClient); + } @Bean ConfigurationEntriesService configurationEntriesService(RestClient restClient) { @@ -220,8 +228,9 @@ ConfigurationEntriesService configurationEntriesService(RestClient restClient) { @Bean public InventoryManager inventoryManager(RestClient restClient, ConfigurationEntriesCache configurationEntriesCache, - PieceStorageService pieceStorageService, InventoryCache inventoryCache, InventoryService inventoryService) { - return new InventoryManager(restClient, configurationEntriesCache, pieceStorageService, inventoryCache, inventoryService); + PieceStorageService pieceStorageService, InventoryCache inventoryCache, InventoryService inventoryService, + ConsortiumConfigurationService consortiumConfigurationService, SharingInstanceService sharingInstanceService) { + return new InventoryManager(restClient, configurationEntriesCache, pieceStorageService, inventoryCache, inventoryService, sharingInstanceService, consortiumConfigurationService); } @Bean @@ -242,8 +251,9 @@ ConfigurationEntriesCache configurationEntriesCache(ConfigurationEntriesService RestClient restClient, OrderLinePatchOperationHandlerResolver orderLinePatchOperationHandlerResolver, PurchaseOrderLineService purchaseOrderLineService, - InventoryCache inventoryCache) { - return new OrderLinePatchOperationService(restClient, orderLinePatchOperationHandlerResolver, purchaseOrderLineService, inventoryCache); + InventoryCache inventoryCache, + InventoryManager inventoryManager) { + return new OrderLinePatchOperationService(restClient, orderLinePatchOperationHandlerResolver, purchaseOrderLineService, inventoryCache, inventoryManager); } @Bean PatchOperationHandler orderLineUpdateInstanceHandler(