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 getTitleById(String titleId, RequestContext requestContext)
}
public Future 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 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 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 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 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(