diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json
index 175a06ef3..f472ce55d 100644
--- a/descriptors/ModuleDescriptor-template.json
+++ b/descriptors/ModuleDescriptor-template.json
@@ -1179,6 +1179,31 @@
}
]
},
+ {
+ "id": "pieces.send-claims",
+ "version": "1.0",
+ "handlers": [
+ {
+ "methods": ["POST"],
+ "pathPattern": "/pieces/claim",
+ "permissionsRequired": [
+ "pieces.send-claims.collection.post"
+ ],
+ "modulePermissions": [
+ "configuration.entries.collection.get",
+ "orders-storage.pieces.collection.get",
+ "orders-storage.pieces.item.put",
+ "orders-storage.po-lines.item.get",
+ "orders-storage.po-lines.item.put",
+ "orders-storage.purchase-orders.item.get",
+ "orders-storage.titles.item.get",
+ "organizations-storage.organizations.item.get",
+ "data-export.job.item.post",
+ "data-export.job.send.item.execute"
+ ]
+ }
+ ]
+ },
{
"id": "_jsonSchemas",
"version": "1.0",
@@ -1421,6 +1446,10 @@
{
"id": "consortia",
"version": "1.0"
+ },
+ {
+ "id": "data-export-spring",
+ "version": "2.0"
}
],
"permissionSets": [
@@ -1993,7 +2022,19 @@
"orders.bind-pieces.item.delete"
]
},
-
+ {
+ "permissionName": "pieces.send-claims.collection.post",
+ "displayName" : "send-claims collection post",
+ "description" : "Send claims collection post"
+ },
+ {
+ "permissionName": "pieces.send-claims.all",
+ "displayName": "All send claims perms",
+ "description": "All permissions for the send claims",
+ "subPermissions": [
+ "pieces.send-claims.collection.post"
+ ]
+ },
{
"permissionName": "orders.all",
"displayName": "orders - all permissions",
@@ -2030,7 +2071,8 @@
"orders.acquisition-methods.all",
"orders.export-history.all",
"orders.routing-lists.all",
- "orders.bind-pieces.all"
+ "orders.bind-pieces.all",
+ "pieces.send-claims.all"
]
},
{
diff --git a/ramls/claim.raml b/ramls/claim.raml
new file mode 100644
index 000000000..cccced5ea
--- /dev/null
+++ b/ramls/claim.raml
@@ -0,0 +1,39 @@
+#%RAML 1.0
+title: Claim
+baseUri: https://github.com/folio-org/mod-orders
+version: v1
+protocols: [ HTTP, HTTPS ]
+
+documentation:
+ - title: Pieces Business Logic API
+ content: API for claiming pieces
+
+types:
+ claiming-collection: !include acq-models/mod-orders/schemas/claimingCollection.json
+ claiming-results: !include acq-models/mod-orders/schemas/claimingResults.json
+ errors: !include raml-util/schemas/errors.schema
+ UUID:
+ type: string
+ pattern: ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$
+
+traits:
+ validate: !include raml-util/traits/validation.raml
+
+resourceTypes:
+ post-with-200: !include rtypes/post-json-200.raml
+
+/pieces/claim:
+ displayName: Claim pieces
+ description: |
+ Claim pieces. The endpoint is used to:
+ - Claims pieces grouped by organizations
+ - Triggers jobs in mod-data-export per each organization that contains an integration detail
+ type:
+ post-with-200:
+ requestSchema: claiming-collection
+ responseSchema: claiming-results
+ requestExample: !include acq-models/mod-orders/examples/claimingCollection.sample
+ responseExample: !include acq-models/mod-orders/examples/claimingResults.sample
+ is: [validate]
+ post:
+ description: Claim pieces
diff --git a/src/main/java/org/folio/models/claiming/ClaimingError.java b/src/main/java/org/folio/models/claiming/ClaimingError.java
new file mode 100644
index 000000000..4f5c8ab5e
--- /dev/null
+++ b/src/main/java/org/folio/models/claiming/ClaimingError.java
@@ -0,0 +1,17 @@
+package org.folio.models.claiming;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum ClaimingError {
+ CANNOT_SEND_CLAIMS_PIECE_IDS_ARE_EMPTY("Cannot send claims, piece ids are empty"),
+ CANNOT_FIND_PIECES_WITH_LATE_STATUS_TO_PROCESS("Cannot find pieces with LATE status to process"),
+ CANNOT_RETRIEVE_CONFIG_ENTRIES("Cannot retrieve config entries"),
+ CANNOT_GROUP_PIECES_BY_VENDOR_MESSAGE("Cannot group pieces by vendor"),
+ CANNOT_CREATE_JOBS_AND_UPDATE_PIECES("Cannot create jobs and update pieces"),
+ CANNOT_FIND_A_PIECE_BY_ID("Cannot find a piece by '%s' id");
+
+ private final String value;
+}
diff --git a/src/main/java/org/folio/models/claiming/IntegrationDetailField.java b/src/main/java/org/folio/models/claiming/IntegrationDetailField.java
new file mode 100644
index 000000000..bf75daa7d
--- /dev/null
+++ b/src/main/java/org/folio/models/claiming/IntegrationDetailField.java
@@ -0,0 +1,14 @@
+package org.folio.models.claiming;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum IntegrationDetailField {
+ EXPORT_TYPE_SPECIFIC_PARAMETERS("exportTypeSpecificParameters"),
+ VENDOR_EDI_ORDERS_EXPORT_CONFIG("vendorEdiOrdersExportConfig"),
+ CLAIM_PIECE_IDS("claimPieceIds");
+
+ private final String value;
+}
diff --git a/src/main/java/org/folio/orders/utils/HelperUtils.java b/src/main/java/org/folio/orders/utils/HelperUtils.java
index aea701b2a..a12b3bd1c 100644
--- a/src/main/java/org/folio/orders/utils/HelperUtils.java
+++ b/src/main/java/org/folio/orders/utils/HelperUtils.java
@@ -65,6 +65,7 @@ public class HelperUtils {
public static final String SYSTEM_CONFIG_MODULE_NAME = "ORG";
public static final String ORDER_CONFIG_MODULE_NAME = "ORDERS";
+ public static final String DATA_EXPORT_SPRING_CONFIG_MODULE_NAME = "mod-data-export-spring";
public static final String DEFAULT_POLINE_LIMIT = "1";
public static final String REASON_COMPLETE = "Complete";
diff --git a/src/main/java/org/folio/orders/utils/ResourcePathResolver.java b/src/main/java/org/folio/orders/utils/ResourcePathResolver.java
index cb03de7b2..aa8e91a94 100644
--- a/src/main/java/org/folio/orders/utils/ResourcePathResolver.java
+++ b/src/main/java/org/folio/orders/utils/ResourcePathResolver.java
@@ -14,6 +14,7 @@ private ResourcePathResolver() {
public static final String PO_LINES_BATCH_STORAGE = "poLines.batch";
public static final String PO_LINES_BUSINESS = "poLinesBusinessEndpoint";
public static final String ORDERS_BUSINESS = "ordersBusinessEndpoint";
+ public static final String PIECES_CLAIMING_BUSINESS = "piecesClaimingBusinessEndpoint";
public static final String PO_NUMBER = "poNumber";
public static final String VENDOR_ID = "vendor";
public static final String PO_LINE_NUMBER = "poLineNumber";
@@ -25,6 +26,7 @@ private ResourcePathResolver() {
public static final String REPORTING_CODES = "reportingCodes";
public static final String PURCHASE_ORDER_STORAGE = "purchaseOrder";
public static final String PIECES_STORAGE = "pieces";
+ public static final String ORGANIZATION_STORAGE = "organizations";
public static final String RECEIVING_HISTORY = "receiving-history";
public static final String RECEIPT_STATUS = "receiptStatus";
public static final String PAYMENT_STATUS = "paymentStatus";
@@ -56,7 +58,8 @@ private ResourcePathResolver() {
public static final String ORDER_SETTINGS = "orderSettings";
public static final String USERS = "users";
public static final String CONSORTIA_USER_TENANTS = "consortia.user-tenants";
-
+ public static final String DATA_EXPORT_SPRING_CREATE_JOB = "data-export-spring.job";
+ public static final String DATA_EXPORT_SPRING_EXECUTE_JOB = "data-export-spring.send-job";
private static final Map SUB_OBJECT_ITEM_APIS;
private static final Map SUB_OBJECT_COLLECTION_APIS;
@@ -72,9 +75,11 @@ private ResourcePathResolver() {
apis.put(PO_LINES_BATCH_STORAGE, "/orders-storage/po-lines-batch");
apis.put(PO_LINES_BUSINESS, "/orders/order-lines");
apis.put(ORDERS_BUSINESS, "/orders/composite-orders");
+ apis.put(PIECES_CLAIMING_BUSINESS, "/pieces/claim");
apis.put(PO_NUMBER, "/orders-storage/po-number");
apis.put(PURCHASE_ORDER_STORAGE, "/orders-storage/purchase-orders");
apis.put(PIECES_STORAGE, "/orders-storage/pieces");
+ apis.put(ORGANIZATION_STORAGE, "/organizations-storage/organizations");
apis.put(RECEIVING_HISTORY, "/orders-storage/receiving-history");
apis.put(PO_LINE_NUMBER, "/orders-storage/po-line-number");
apis.put(ORDER_TEMPLATES, "/orders-storage/order-templates");
@@ -105,6 +110,8 @@ private ResourcePathResolver() {
apis.put(CONSORTIA_USER_TENANTS, "/consortia/{id}/user-tenants");
apis.put(ORDER_SETTINGS, "/orders-storage/settings");
apis.put(ROUTING_LISTS, "/orders-storage/routing-lists");
+ apis.put(DATA_EXPORT_SPRING_CREATE_JOB, "/data-export-spring/jobs");
+ apis.put(DATA_EXPORT_SPRING_EXECUTE_JOB, "/data-export-spring/jobs/send");
SUB_OBJECT_COLLECTION_APIS = Collections.unmodifiableMap(apis);
SUB_OBJECT_ITEM_APIS = Collections.unmodifiableMap(
diff --git a/src/main/java/org/folio/rest/impl/PiecesClaimingApi.java b/src/main/java/org/folio/rest/impl/PiecesClaimingApi.java
new file mode 100644
index 000000000..ccf26a1d4
--- /dev/null
+++ b/src/main/java/org/folio/rest/impl/PiecesClaimingApi.java
@@ -0,0 +1,46 @@
+package org.folio.rest.impl;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Context;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import org.folio.rest.annotations.Validate;
+import org.folio.rest.core.models.RequestContext;
+import org.folio.rest.jaxrs.model.ClaimingCollection;
+import org.folio.rest.jaxrs.resource.PiecesClaim;
+import org.folio.service.pieces.PiecesClaimingService;
+import org.folio.spring.SpringContextUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import javax.ws.rs.core.Response;
+import java.util.Map;
+
+import static org.folio.orders.utils.ResourcePathResolver.PIECES_CLAIMING_BUSINESS;
+import static org.folio.orders.utils.ResourcePathResolver.resourceByIdPath;
+import static org.folio.rest.RestConstants.OKAPI_URL;
+
+public class PiecesClaimingApi extends BaseApi implements PiecesClaim {
+
+ @Autowired
+ private PiecesClaimingService pieceClaimingService;
+
+ public PiecesClaimingApi() {
+ SpringContextUtil.autowireDependencies(this, Vertx.currentContext());
+ }
+
+ @Override
+ @Validate
+ public void postPiecesClaim(ClaimingCollection claimingCollection, Map okapiHeaders,
+ Handler> asyncResultHandler, Context vertxContext) {
+ var requestContext = new RequestContext(vertxContext, okapiHeaders);
+ pieceClaimingService.sendClaims(claimingCollection, requestContext)
+ .onSuccess(claimingResults -> {
+ var okapiUrl = okapiHeaders.get(OKAPI_URL);
+ var url = resourceByIdPath(PIECES_CLAIMING_BUSINESS);
+ var response = buildResponseWithLocation(okapiUrl, url, claimingResults);
+ asyncResultHandler.handle(Future.succeededFuture(response));
+ })
+ .onFailure(t -> handleErrorResponse(asyncResultHandler, t));
+ }
+}
diff --git a/src/main/java/org/folio/service/pieces/PieceStorageService.java b/src/main/java/org/folio/service/pieces/PieceStorageService.java
index b89a19eb2..466cab229 100644
--- a/src/main/java/org/folio/service/pieces/PieceStorageService.java
+++ b/src/main/java/org/folio/service/pieces/PieceStorageService.java
@@ -13,7 +13,6 @@
import java.util.Collection;
import java.util.Collections;
import java.util.List;
-import java.util.stream.Collectors;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.collections4.CollectionUtils;
@@ -135,6 +134,7 @@ public Future getAllPieces(String query, RequestContext request
}
public Future getAllPieces(int limit, int offset, String query, RequestContext requestContext) {
+ log.debug("getAllPieces:: limit: {}, offset: {}, query: {}", limit, offset, query);
var requestEntry = new RequestEntry(PIECE_STORAGE_ENDPOINT).withQuery(query).withOffset(offset).withLimit(limit);
return restClient.get(requestEntry, PieceCollection.class, requestContext);
}
@@ -165,20 +165,18 @@ public Future> getPiecesByIds(List pieceIds, RequestContext
.map(PieceCollection::getPieces)
.flatMap(Collection::stream)
.toList())
- .onSuccess(v -> log.info("getPiecesByIds:: pieces by ids successfully retrieve: {}", pieceIds));
+ .onSuccess(v -> log.info("getPiecesByIds:: pieces by ids successfully retrieve: {}", pieceIds))
+ .onFailure(t -> log.error("Failed to get pieces by ids", t));
}
public Future> getPiecesByLineIdsByChunks(List lineIds, RequestContext requestContext) {
- log.info("getPiecesByLineIdsByChunks start");
var futures = ofSubLists(new ArrayList<>(lineIds), MAX_IDS_FOR_GET_RQ_15)
.map(ids -> getPieceChunkByLineIds(ids, requestContext))
.toList();
return collectResultsOnSuccess(futures)
.map(lists -> lists.stream()
.flatMap(Collection::stream)
- .collect(Collectors.toList()))
- .onSuccess(v -> log.info("getPiecesByLineIdsByChunks end"));
-
+ .toList());
}
private Future> getPieceChunkByLineIds(Collection poLineIds, RequestContext requestContext) {
diff --git a/src/main/java/org/folio/service/pieces/PiecesClaimingService.java b/src/main/java/org/folio/service/pieces/PiecesClaimingService.java
new file mode 100644
index 000000000..a2fdcf561
--- /dev/null
+++ b/src/main/java/org/folio/service/pieces/PiecesClaimingService.java
@@ -0,0 +1,232 @@
+package org.folio.service.pieces;
+
+import io.vertx.core.Future;
+import io.vertx.core.json.JsonObject;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import one.util.streamex.StreamEx;
+import org.apache.commons.lang3.tuple.Pair;
+import org.folio.rest.core.RestClient;
+import org.folio.rest.core.models.RequestContext;
+import org.folio.rest.jaxrs.model.ClaimingCollection;
+import org.folio.rest.jaxrs.model.ClaimingPieceResult;
+import org.folio.rest.jaxrs.model.ClaimingResults;
+import org.folio.rest.jaxrs.model.Error;
+import org.folio.rest.jaxrs.model.Piece;
+import org.folio.rest.jaxrs.model.PieceBatchStatusCollection;
+import org.folio.service.caches.ConfigurationEntriesCache;
+import org.folio.service.orders.PurchaseOrderLineService;
+import org.folio.service.orders.PurchaseOrderStorageService;
+import org.folio.service.organization.OrganizationService;
+import org.folio.service.pieces.flows.update.PieceUpdateFlowManager;
+import org.springframework.stereotype.Service;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+
+import static java.util.stream.Collectors.mapping;
+import static java.util.stream.Collectors.toList;
+import static org.folio.models.claiming.ClaimingError.CANNOT_CREATE_JOBS_AND_UPDATE_PIECES;
+import static org.folio.models.claiming.ClaimingError.CANNOT_FIND_A_PIECE_BY_ID;
+import static org.folio.models.claiming.ClaimingError.CANNOT_FIND_PIECES_WITH_LATE_STATUS_TO_PROCESS;
+import static org.folio.models.claiming.ClaimingError.CANNOT_GROUP_PIECES_BY_VENDOR_MESSAGE;
+import static org.folio.models.claiming.ClaimingError.CANNOT_RETRIEVE_CONFIG_ENTRIES;
+import static org.folio.models.claiming.ClaimingError.CANNOT_SEND_CLAIMS_PIECE_IDS_ARE_EMPTY;
+import static org.folio.models.claiming.IntegrationDetailField.CLAIM_PIECE_IDS;
+import static org.folio.models.claiming.IntegrationDetailField.EXPORT_TYPE_SPECIFIC_PARAMETERS;
+import static org.folio.models.claiming.IntegrationDetailField.VENDOR_EDI_ORDERS_EXPORT_CONFIG;
+import static org.folio.orders.utils.HelperUtils.DATA_EXPORT_SPRING_CONFIG_MODULE_NAME;
+import static org.folio.orders.utils.HelperUtils.collectResultsOnSuccess;
+import static org.folio.orders.utils.ResourcePathResolver.DATA_EXPORT_SPRING_CREATE_JOB;
+import static org.folio.orders.utils.ResourcePathResolver.DATA_EXPORT_SPRING_EXECUTE_JOB;
+import static org.folio.orders.utils.ResourcePathResolver.resourcesPath;
+import static org.folio.rest.jaxrs.model.ClaimingPieceResult.Status.FAILURE;
+import static org.folio.rest.jaxrs.model.ClaimingPieceResult.Status.SUCCESS;
+
+@Log4j2
+@Service
+@RequiredArgsConstructor
+public class PiecesClaimingService {
+
+ private static final String JOB_STATUS = "status";
+ private static final String EXPORT_TYPE_CLAIMS = "CLAIMS";
+
+ private final ConfigurationEntriesCache configurationEntriesCache;
+ private final PieceStorageService pieceStorageService;
+ private final PurchaseOrderLineService purchaseOrderLineService;
+ private final PurchaseOrderStorageService purchaseOrderStorageService;
+ private final OrganizationService organizationService;
+ private final PieceUpdateFlowManager pieceUpdateFlowManager;
+ private final RestClient restClient;
+
+ /**
+ * Sends claims by receiving pieces to be claimed, groups them by vendor,
+ * updates piece statuses and finally creates jobs per vendor and associated integration details
+ * @param claimingCollection An array of pieces ids
+ * @param requestContext Headers to make HTTP or Kafka requests
+ * @return Future of an array of claimingResults
+ */
+ public Future sendClaims(ClaimingCollection claimingCollection, RequestContext requestContext) {
+ if (CollectionUtils.isEmpty(claimingCollection.getClaimingPieceIds())) {
+ log.info("sendClaims:: No claims are sent, claiming piece ids are empty");
+ return Future.succeededFuture(createEmptyClaimingResults(CANNOT_SEND_CLAIMS_PIECE_IDS_ARE_EMPTY.getValue()));
+ }
+ return configurationEntriesCache.loadConfiguration(DATA_EXPORT_SPRING_CONFIG_MODULE_NAME, requestContext)
+ .compose(config -> {
+ if (CollectionUtils.isEmpty(config.getMap())) {
+ log.info("sendClaims:: No claims are sent, config has no entries");
+ return Future.succeededFuture(createEmptyClaimingResults(CANNOT_RETRIEVE_CONFIG_ENTRIES.getValue()));
+ }
+ var pieceIds = claimingCollection.getClaimingPieceIds().stream().toList();
+ log.info("sendClaims:: Received pieces to be claimed, pieceIds: {}", pieceIds);
+ return groupPieceIdsByVendorId(pieceIds, requestContext)
+ .compose(pieceIdsByVendorIds -> {
+ if (CollectionUtils.isEmpty(pieceIdsByVendorIds)) {
+ return Future.succeededFuture(createEmptyClaimingResults(CANNOT_FIND_PIECES_WITH_LATE_STATUS_TO_PROCESS.getValue()));
+ }
+ log.info("sendClaims:: Using pieces by vendor id map, map: {}", pieceIdsByVendorIds);
+ return createJobsByVendor(config, pieceIdsByVendorIds, requestContext);
+ });
+ })
+ .onFailure(t -> log.error("sendClaims:: Failed send claims: {}", JsonObject.mapFrom(claimingCollection).encodePrettily(), t));
+ }
+
+ private Future