diff --git a/NEWS.md b/NEWS.md index 18d5c1f0..a4ffae8b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,10 @@ +## v1.2.3 2024-12-02 +* MODDCB-90: Accept existing circulation request ID +* MODDCB-105: Accept existing circulation request ID (borrowing transaction) +* MODDCB-111: Allow manual transaction status change from CREATED to OPEN +* MODDCB-117: Add ecsRequestPhase to the circulation request schema +* MODDCB-124: Merge esc-tlr feature branch into master + ## v1.2.2 2024-11-20 * MODDCB-145: Issue with spaces in service point name diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index a103410c..1b52f2b1 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -144,6 +144,33 @@ } ] }, + { + "id": "ecs-request-transactions", + "version": "1.0", + "handlers": [ + { + "methods": [ + "POST" + ], + "pathPattern": "/ecs-request-transactions/{ecsRequestTransactionId}", + "permissionsRequired": [ + "dcb.ecs-request.transactions.post" + ], + "modulePermissions": [ + "circulation-storage.requests.item.get", + "circulation-storage.requests.collection.get", + "circulation.requests.item.put", + "inventory-storage.items.item.get", + "inventory-storage.items.collection.get", + "circulation-item.item.post", + "circulation-item.collection.get", + "circulation-item.item.get", + "inventory-storage.material-types.collection.get", + "inventory-storage.loan-types.collection.get" + ] + } + ] + }, { "id": "_tenant", "version": "2.0", @@ -204,7 +231,8 @@ "dcb.transactions.post", "dcb.transactions.put", "dcb.transactions.get", - "dcb.transactions.collection.get" + "dcb.transactions.collection.get", + "dcb.ecs-request.transactions.post" ] }, { @@ -226,6 +254,11 @@ "permissionName": "dcb.transactions.collection.get", "displayName": "get updated transaction detail list", "description": "get list of transaction updated between a given query range" + }, + { + "permissionName": "dcb.ecs-request.transactions.post", + "displayName": "creates new ECS request transaction", + "description": "creates new ECS request transaction" } ], "metadata": { diff --git a/pom.xml b/pom.xml index 54fe3d4b..8649c093 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ org.folio mod-dcb mod-dcb - 1.2.3-SNAPSHOT + 1.2.4-SNAPSHOT Manage DCB related transactions in folio jar diff --git a/src/main/java/org/folio/dcb/client/feign/CirculationClient.java b/src/main/java/org/folio/dcb/client/feign/CirculationClient.java index 40340069..8b21366f 100644 --- a/src/main/java/org/folio/dcb/client/feign/CirculationClient.java +++ b/src/main/java/org/folio/dcb/client/feign/CirculationClient.java @@ -22,5 +22,6 @@ public interface CirculationClient { void checkOutByBarcode(@RequestBody CheckOutRequest checkOutRequest); @PutMapping("/requests/{requestId}") - CirculationRequest cancelRequest(@PathVariable("requestId") String requestId, @RequestBody CirculationRequest circulationRequest); + CirculationRequest updateRequest(@PathVariable("requestId") String requestId, + @RequestBody CirculationRequest circulationRequest); } diff --git a/src/main/java/org/folio/dcb/controller/EcsRequestTransactionsApiController.java b/src/main/java/org/folio/dcb/controller/EcsRequestTransactionsApiController.java new file mode 100644 index 00000000..e85b53b7 --- /dev/null +++ b/src/main/java/org/folio/dcb/controller/EcsRequestTransactionsApiController.java @@ -0,0 +1,39 @@ +package org.folio.dcb.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.folio.dcb.domain.dto.DcbTransaction; +import org.folio.dcb.domain.dto.TransactionStatusResponse; +import org.folio.dcb.rest.resource.EcsRequestTransactionsApi; +import org.folio.dcb.service.EcsRequestTransactionsService; +import org.folio.dcb.service.TransactionAuditService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Log4j2 +@RequiredArgsConstructor +public class EcsRequestTransactionsApiController implements EcsRequestTransactionsApi { + + private final EcsRequestTransactionsService ecsRequestTransactionsService; + private final TransactionAuditService transactionAuditService; + + @Override + public ResponseEntity createEcsRequestTransactions( + String ecsRequestTransactionId, DcbTransaction dcbTransaction) { + + log.info("createEcsRequestTransactions:: creating ECS Request Transaction {} with ID {}", + dcbTransaction, ecsRequestTransactionId); + TransactionStatusResponse transactionStatusResponse; + try { + transactionStatusResponse = ecsRequestTransactionsService.createEcsRequestTransactions( + ecsRequestTransactionId, dcbTransaction); + } catch (Exception ex) { + transactionAuditService.logErrorIfTransactionAuditNotExists(ecsRequestTransactionId, + dcbTransaction, ex.getMessage()); + throw ex; + } + return ResponseEntity.status(HttpStatus.CREATED).body(transactionStatusResponse); + } +} diff --git a/src/main/java/org/folio/dcb/listener/kafka/CirculationEventListener.java b/src/main/java/org/folio/dcb/listener/kafka/CirculationEventListener.java index 0011f62d..49069a2a 100644 --- a/src/main/java/org/folio/dcb/listener/kafka/CirculationEventListener.java +++ b/src/main/java/org/folio/dcb/listener/kafka/CirculationEventListener.java @@ -71,7 +71,7 @@ public void handleLoanEvent(String data, MessageHeaders messageHeaders) { public void handleRequestEvent(String data, MessageHeaders messageHeaders) { String tenantId = getHeaderValue(messageHeaders, XOkapiHeaders.TENANT, null).get(0); var eventData = parseRequestEvent(data); - if (Objects.nonNull(eventData) && eventData.isDcb() ) { + if (Objects.nonNull(eventData)) { log.debug("dcb flow for a request event"); String requestId = eventData.getRequestId(); if (Objects.nonNull(requestId)) { diff --git a/src/main/java/org/folio/dcb/service/CirculationRequestService.java b/src/main/java/org/folio/dcb/service/CirculationRequestService.java index d4624e78..6cfde41f 100644 --- a/src/main/java/org/folio/dcb/service/CirculationRequestService.java +++ b/src/main/java/org/folio/dcb/service/CirculationRequestService.java @@ -4,4 +4,5 @@ public interface CirculationRequestService { CirculationRequest getCancellationRequestIfOpenOrNull(String requestId); + CirculationRequest fetchRequestById(String requestId); } diff --git a/src/main/java/org/folio/dcb/service/EcsRequestTransactionsService.java b/src/main/java/org/folio/dcb/service/EcsRequestTransactionsService.java new file mode 100644 index 00000000..dcb44c41 --- /dev/null +++ b/src/main/java/org/folio/dcb/service/EcsRequestTransactionsService.java @@ -0,0 +1,9 @@ +package org.folio.dcb.service; + +import org.folio.dcb.domain.dto.DcbTransaction; +import org.folio.dcb.domain.dto.TransactionStatusResponse; + +public interface EcsRequestTransactionsService { + TransactionStatusResponse createEcsRequestTransactions(String ecsRequestTransactionsId, + DcbTransaction dcbTransaction); +} diff --git a/src/main/java/org/folio/dcb/service/RequestService.java b/src/main/java/org/folio/dcb/service/RequestService.java index 0ac6842f..49358cb8 100644 --- a/src/main/java/org/folio/dcb/service/RequestService.java +++ b/src/main/java/org/folio/dcb/service/RequestService.java @@ -12,4 +12,5 @@ public interface RequestService { */ CirculationRequest createPageItemRequest(User user, DcbItem dcbItem, String pickupServicePointId); CirculationRequest createHoldItemRequest(User user, DcbItem dcbItem, String pickupServicePointId); + void updateCirculationRequest(CirculationRequest circulationRequest); } diff --git a/src/main/java/org/folio/dcb/service/StatusProcessorService.java b/src/main/java/org/folio/dcb/service/StatusProcessorService.java index f76b6f4c..583a1b6a 100644 --- a/src/main/java/org/folio/dcb/service/StatusProcessorService.java +++ b/src/main/java/org/folio/dcb/service/StatusProcessorService.java @@ -28,7 +28,7 @@ public List lendingChainProcessor(TransactionStatu StatusProcessor checkInProcessor = new StatusProcessor(ITEM_CHECKED_OUT, ITEM_CHECKED_IN, false, closeProcessor); StatusProcessor checkoutProcessor = new StatusProcessor(AWAITING_PICKUP, ITEM_CHECKED_OUT, false, checkInProcessor); StatusProcessor awaitingPickupProcessor = new StatusProcessor(OPEN, AWAITING_PICKUP, false, checkoutProcessor); - StatusProcessor openProcessor = new StatusProcessor(CREATED, OPEN, true, awaitingPickupProcessor); + StatusProcessor openProcessor = new StatusProcessor(CREATED, OPEN, false, awaitingPickupProcessor); startChain.setChain(openProcessor); var statuses = process(startChain, fromStatus, toStatus); log.info("lendingChainProcessor:: Following statuses needs to be transitioned {} ", statuses); diff --git a/src/main/java/org/folio/dcb/service/impl/CirculationRequestServiceImpl.java b/src/main/java/org/folio/dcb/service/impl/CirculationRequestServiceImpl.java index 64700355..51410422 100644 --- a/src/main/java/org/folio/dcb/service/impl/CirculationRequestServiceImpl.java +++ b/src/main/java/org/folio/dcb/service/impl/CirculationRequestServiceImpl.java @@ -22,7 +22,7 @@ public class CirculationRequestServiceImpl implements CirculationRequestService private final CirculationRequestClient circulationRequestClient; private final FolioExecutionContext folioExecutionContext; - private CirculationRequest fetchRequestById(String requestId) { + public CirculationRequest fetchRequestById(String requestId) { log.info("fetchRequestById:: fetching request for id {} ", requestId); try { return circulationRequestClient.fetchRequestById(requestId); diff --git a/src/main/java/org/folio/dcb/service/impl/CirculationServiceImpl.java b/src/main/java/org/folio/dcb/service/impl/CirculationServiceImpl.java index 9c88acd2..9934385f 100644 --- a/src/main/java/org/folio/dcb/service/impl/CirculationServiceImpl.java +++ b/src/main/java/org/folio/dcb/service/impl/CirculationServiceImpl.java @@ -46,7 +46,7 @@ public void cancelRequest(TransactionEntity dcbTransaction) { CirculationRequest request = circulationStorageService.getCancellationRequestIfOpenOrNull(dcbTransaction.getRequestId().toString()); if (request != null){ try { - circulationClient.cancelRequest(request.getId(), request); + circulationClient.updateRequest(request.getId(), request); } catch (FeignException e) { log.warn("cancelRequest:: error cancelling request using request id {} ", dcbTransaction.getRequestId(), e); throw new CirculationRequestException(String.format("Error cancelling request using request id %s", dcbTransaction.getRequestId())); diff --git a/src/main/java/org/folio/dcb/service/impl/EcsRequestTransactionsServiceImpl.java b/src/main/java/org/folio/dcb/service/impl/EcsRequestTransactionsServiceImpl.java new file mode 100644 index 00000000..722660b2 --- /dev/null +++ b/src/main/java/org/folio/dcb/service/impl/EcsRequestTransactionsServiceImpl.java @@ -0,0 +1,119 @@ +package org.folio.dcb.service.impl; + +import static org.folio.dcb.domain.dto.DcbTransaction.RoleEnum.BORROWER; +import static org.folio.dcb.domain.dto.DcbTransaction.RoleEnum.LENDER; + +import java.util.UUID; + +import org.folio.dcb.domain.dto.CirculationItem; +import org.folio.dcb.domain.dto.CirculationRequest; +import org.folio.dcb.domain.dto.DcbItem; +import org.folio.dcb.domain.dto.DcbPatron; +import org.folio.dcb.domain.dto.DcbPickup; +import org.folio.dcb.domain.dto.DcbTransaction; +import org.folio.dcb.domain.dto.Item; +import org.folio.dcb.domain.dto.TransactionStatusResponse; +import org.folio.dcb.exception.ResourceAlreadyExistException; +import org.folio.dcb.repository.TransactionRepository; +import org.folio.dcb.service.CirculationItemService; +import org.folio.dcb.service.CirculationRequestService; +import org.folio.dcb.service.EcsRequestTransactionsService; +import org.folio.dcb.service.RequestService; +import org.folio.dcb.utils.RequestStatus; +import org.springframework.stereotype.Service; + +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@Service +@RequiredArgsConstructor +@Log4j2 +public class EcsRequestTransactionsServiceImpl implements EcsRequestTransactionsService { + + private final BaseLibraryService baseLibraryService; + private final TransactionRepository transactionRepository; + private final RequestService requestService; + private final CirculationRequestService circulationRequestService; + private final CirculationItemService circulationItemService; + + @Override + public TransactionStatusResponse createEcsRequestTransactions(String ecsRequestTransactionsId, + DcbTransaction dcbTransaction) { + + log.info("createEcsRequestTransactions:: creating new transaction request for role {} ", + dcbTransaction.getRole()); + checkEcsRequestTransactionExistsAndThrow(ecsRequestTransactionsId); + CirculationRequest circulationRequest = circulationRequestService.fetchRequestById( + dcbTransaction.getRequestId()); + if (circulationRequest != null && RequestStatus.isRequestOpen( + RequestStatus.from(circulationRequest.getStatus()))) { + if (dcbTransaction.getRole() == LENDER) { + createLenderEcsRequestTransactions(ecsRequestTransactionsId, dcbTransaction, circulationRequest); + } else if(dcbTransaction.getRole() == BORROWER) { + createBorrowerEcsRequestTransactions(ecsRequestTransactionsId, dcbTransaction, + circulationRequest); + } else { + throw new IllegalArgumentException("Unimplemented role: " + dcbTransaction.getRole()); + } + return TransactionStatusResponse.builder() + .status(TransactionStatusResponse.StatusEnum.CREATED) + .item(dcbTransaction.getItem()) + .patron(dcbTransaction.getPatron()) + .build(); + } else { + throw new IllegalArgumentException("Unable to create ECS transaction as could not find open request"); + } + } + + private void checkEcsRequestTransactionExistsAndThrow(String dcbTransactionId) { + if (transactionRepository.existsById(dcbTransactionId)) { + throw new ResourceAlreadyExistException( + String.format("unable to create ECS transaction with ID %s as it already exists", + dcbTransactionId)); + } + } + + private void createLenderEcsRequestTransactions(String ecsRequestTransactionsId, + DcbTransaction dcbTransaction, CirculationRequest circulationRequest) { + + dcbTransaction.setItem(DcbItem.builder() + .id(String.valueOf(circulationRequest.getItemId())) + .barcode(circulationRequest.getItem().getBarcode()) + .build()); + dcbTransaction.setPatron(DcbPatron.builder() + .id(String.valueOf(circulationRequest.getRequesterId())) + .barcode(circulationRequest.getRequester().getBarcode()) + .build()); + dcbTransaction.setPickup(DcbPickup.builder() + .servicePointId(String.valueOf(circulationRequest.getPickupServicePointId())) + .build()); + baseLibraryService.saveDcbTransaction(ecsRequestTransactionsId, dcbTransaction, + dcbTransaction.getRequestId()); + } + + private void createBorrowerEcsRequestTransactions(String ecsRequestTransactionsId, + DcbTransaction dcbTransaction, CirculationRequest circulationRequest) { + + var itemVirtual = dcbTransaction.getItem(); + if (itemVirtual == null) { + throw new IllegalArgumentException("Item is required for borrower transaction"); + } + baseLibraryService.checkItemExistsInInventoryAndThrow(itemVirtual.getBarcode()); + CirculationItem item = circulationItemService.checkIfItemExistsAndCreate(itemVirtual, circulationRequest.getPickupServicePointId()); + circulationRequest.setItemId(UUID.fromString(item.getId())); + circulationRequest.setItem(Item.builder() + .barcode(item.getBarcode()) + .build()); + circulationRequest.setHoldingsRecordId(UUID.fromString(item.getHoldingsRecordId())); + requestService.updateCirculationRequest(circulationRequest); + dcbTransaction.setPatron(DcbPatron.builder() + .id(String.valueOf(circulationRequest.getRequesterId())) + .barcode(circulationRequest.getRequester().getBarcode()) + .build()); + dcbTransaction.setPickup(DcbPickup.builder() + .servicePointId(String.valueOf(circulationRequest.getPickupServicePointId())) + .build()); + baseLibraryService.saveDcbTransaction(ecsRequestTransactionsId, dcbTransaction, + dcbTransaction.getRequestId()); + } +} diff --git a/src/main/java/org/folio/dcb/service/impl/LendingLibraryServiceImpl.java b/src/main/java/org/folio/dcb/service/impl/LendingLibraryServiceImpl.java index 56d38bff..de65af3e 100644 --- a/src/main/java/org/folio/dcb/service/impl/LendingLibraryServiceImpl.java +++ b/src/main/java/org/folio/dcb/service/impl/LendingLibraryServiceImpl.java @@ -17,6 +17,7 @@ import org.springframework.stereotype.Service; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.AWAITING_PICKUP; +import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.CREATED; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.ITEM_CHECKED_IN; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.ITEM_CHECKED_OUT; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.OPEN; @@ -61,7 +62,9 @@ public void updateTransactionStatus(TransactionEntity dcbTransaction, Transactio log.debug("updateTransactionStatus:: Updating dcbTransaction {} to status {} ", dcbTransaction, transactionStatus); var currentStatus = dcbTransaction.getStatus(); var requestedStatus = transactionStatus.getStatus(); - if (OPEN == currentStatus && AWAITING_PICKUP == requestedStatus) { + if (CREATED == currentStatus && OPEN == requestedStatus) { + updateTransactionEntity(dcbTransaction, requestedStatus); + } else if (OPEN == currentStatus && AWAITING_PICKUP == requestedStatus) { log.info("updateTransactionStatus:: Checking in item by barcode: {} ", dcbTransaction.getItemBarcode()); circulationService.checkInByBarcode(dcbTransaction); updateTransactionEntity(dcbTransaction, requestedStatus); diff --git a/src/main/java/org/folio/dcb/service/impl/RequestServiceImpl.java b/src/main/java/org/folio/dcb/service/impl/RequestServiceImpl.java index 65c1f8a3..f8fe764f 100644 --- a/src/main/java/org/folio/dcb/service/impl/RequestServiceImpl.java +++ b/src/main/java/org/folio/dcb/service/impl/RequestServiceImpl.java @@ -48,6 +48,13 @@ public CirculationRequest createHoldItemRequest(User user, DcbItem item, String return circulationClient.createRequest(circulationRequest); } + @Override + public void updateCirculationRequest(CirculationRequest circulationRequest) { + log.debug("updateCirculationRequest:: updating circulation request with id {}", + circulationRequest.getId()); + circulationClient.updateRequest(circulationRequest.getId(), circulationRequest); + } + private CirculationRequest createCirculationRequest(CirculationRequest.RequestTypeEnum type, User user, DcbItem item, String holdingsId, String instanceId, String pickupServicePointId) { return CirculationRequest.builder() .id(UUID.randomUUID().toString()) diff --git a/src/main/java/org/folio/dcb/service/impl/TransactionAuditServiceImpl.java b/src/main/java/org/folio/dcb/service/impl/TransactionAuditServiceImpl.java index 5b6a64b1..ba2a9888 100644 --- a/src/main/java/org/folio/dcb/service/impl/TransactionAuditServiceImpl.java +++ b/src/main/java/org/folio/dcb/service/impl/TransactionAuditServiceImpl.java @@ -4,8 +4,6 @@ import lombok.extern.log4j.Log4j2; import org.folio.dcb.domain.dto.DcbTransaction; import org.folio.dcb.domain.entity.TransactionAuditEntity; -import org.folio.dcb.domain.entity.TransactionEntity; -import org.folio.dcb.domain.mapper.TransactionMapper; import org.folio.dcb.repository.TransactionAuditRepository; import org.folio.dcb.service.TransactionAuditService; import org.springframework.stereotype.Service; @@ -20,7 +18,6 @@ public class TransactionAuditServiceImpl implements TransactionAuditService { private static final String DUPLICATE_ERROR_ACTION = "DUPLICATE_ERROR"; private static final String DUPLICATE_ERROR_TRANSACTION_ID = "-1"; - private final TransactionMapper transactionMapper; private final TransactionAuditRepository transactionAuditRepository; @Override public void logErrorIfTransactionAuditExists(String dcbTransactionId, String errorMsg) { @@ -44,8 +41,7 @@ public void logErrorIfTransactionAuditExists(String dcbTransactionId, String err @Override public void logErrorIfTransactionAuditNotExists(String dcbTransactionId, DcbTransaction dcbTransaction, String errorMsg) { TransactionAuditEntity auditExisting = transactionAuditRepository.findLatestTransactionAuditEntityByDcbTransactionId(dcbTransactionId).orElse(null); - TransactionEntity transactionMapped = transactionMapper.mapToEntity(dcbTransactionId, dcbTransaction); - TransactionAuditEntity auditError = generateTrnAuditEntityByTrnEntityWithError(dcbTransactionId, transactionMapped, errorMsg); + TransactionAuditEntity auditError = generateTrnAuditEntityByTrnEntityWithError(dcbTransactionId, dcbTransaction, errorMsg); if (auditExisting != null) { log.debug("logTheErrorForNotExistedTransactionAudit:: dcbTransactionId = {}, dcbTransaction = {}, err = {}", dcbTransactionId, dcbTransaction, errorMsg); @@ -68,7 +64,7 @@ private TransactionAuditEntity generateTrnAuditEntityFromTheFoundOneWithError(Tr return auditError; } - private TransactionAuditEntity generateTrnAuditEntityByTrnEntityWithError(String dcbTransactionId, TransactionEntity trnE, String errorMsg) { + private TransactionAuditEntity generateTrnAuditEntityByTrnEntityWithError(String dcbTransactionId, DcbTransaction trnE, String errorMsg) { String errorMessage = String.format("dcbTransactionId = %s; dcb transaction content = %s; error message = %s.", dcbTransactionId, trnE.toString(), errorMsg); TransactionAuditEntity auditError = new TransactionAuditEntity(); diff --git a/src/main/java/org/folio/dcb/service/impl/TransactionsServiceImpl.java b/src/main/java/org/folio/dcb/service/impl/TransactionsServiceImpl.java index d697931a..64b41d20 100644 --- a/src/main/java/org/folio/dcb/service/impl/TransactionsServiceImpl.java +++ b/src/main/java/org/folio/dcb/service/impl/TransactionsServiceImpl.java @@ -126,9 +126,10 @@ public TransactionEntity getTransactionEntityOrThrow(String dcbTransactionId) { } private void checkTransactionExistsAndThrow(String dcbTransactionId) { - if(transactionRepository.existsById(dcbTransactionId)) { + if (transactionRepository.existsById(dcbTransactionId)) { throw new ResourceAlreadyExistException( String.format("unable to create transaction with id %s as it already exists", dcbTransactionId)); } } + } diff --git a/src/main/resources/swagger.api/dcb_transaction.yaml b/src/main/resources/swagger.api/dcb_transaction.yaml index 21c3d176..5871cfa8 100644 --- a/src/main/resources/swagger.api/dcb_transaction.yaml +++ b/src/main/resources/swagger.api/dcb_transaction.yaml @@ -57,6 +57,26 @@ paths: $ref: '#/components/responses/NotFound' '500': $ref: '#/components/responses/InternalServerError' + /ecs-request-transactions/{ecsRequestTransactionId}: + description: ECS TLR Transaction endpoint + post: + description: Create transaction for existing circulation TLR + operationId: createEcsRequestTransactions + tags: + - ecs-tlr-transaction + parameters: + - $ref: '#/components/parameters/ecsRequestTransactionId' + requestBody: + $ref: "#/components/requestBodies/DCBTransaction" + responses: + '201': + $ref: '#/components/responses/TransactionStatusResponse' + '400': + $ref: '#/components/responses/BadRequest' + '409': + $ref: '#/components/responses/Conflict' + '500': + $ref: '#/components/responses/InternalServerError' /transactions/status: parameters: - $ref: '#/components/parameters/fromDate' @@ -151,6 +171,12 @@ components: schema: type: string required: true + ecsRequestTransactionId: + in: path + name: ecsRequestTransactionId + schema: + type: string + required: true fromDate: in: query name: fromDate diff --git a/src/main/resources/swagger.api/schemas/CirculationRequest.yaml b/src/main/resources/swagger.api/schemas/CirculationRequest.yaml index 8634f6b0..a8fb0c56 100644 --- a/src/main/resources/swagger.api/schemas/CirculationRequest.yaml +++ b/src/main/resources/swagger.api/schemas/CirculationRequest.yaml @@ -12,11 +12,18 @@ CirculationRequest: - Hold - Recall - Page + ecsRequestPhase: + description: Stage in ECS request process, absence of this field means this is a single-tenant request + type: string + enum: + - Primary + - Secondary requestLevel: description: Level of the request - Item or Title type: string enum: - Item + - Title requestDate: description: Date the request was made type: string @@ -43,6 +50,9 @@ CirculationRequest: status: description: Status of the request type: string + position: + description: Position of the request in the queue + type: integer cancellationReasonId: description: The id of the request reason type: string diff --git a/src/main/resources/swagger.api/schemas/dcbTransaction.yaml b/src/main/resources/swagger.api/schemas/dcbTransaction.yaml index 02d16935..8c6be9c2 100644 --- a/src/main/resources/swagger.api/schemas/dcbTransaction.yaml +++ b/src/main/resources/swagger.api/schemas/dcbTransaction.yaml @@ -7,6 +7,9 @@ DcbTransaction: $ref: 'dcbPatron.yaml#/DcbPatron' pickup: $ref: 'dcbPickup.yaml#/DcbPickup' + requestId: + description: ID of the existing circulation TLR + type: string role: type: string enum: diff --git a/src/test/java/org/folio/dcb/controller/EcsRequestTransactionsApiControllerTest.java b/src/test/java/org/folio/dcb/controller/EcsRequestTransactionsApiControllerTest.java new file mode 100644 index 00000000..3b30281f --- /dev/null +++ b/src/test/java/org/folio/dcb/controller/EcsRequestTransactionsApiControllerTest.java @@ -0,0 +1,106 @@ +package org.folio.dcb.controller; + +import static org.folio.dcb.utils.EntityUtils.CIRCULATION_REQUEST_ID; +import static org.folio.dcb.utils.EntityUtils.createBorrowingEcsRequestTransactionByRole; +import static org.folio.dcb.utils.EntityUtils.createLendingEcsRequestTransactionByRole; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.UUID; + +import org.folio.dcb.domain.dto.DcbTransaction; +import org.folio.dcb.domain.entity.TransactionAuditEntity; +import org.folio.dcb.repository.TransactionAuditRepository; +import org.folio.dcb.repository.TransactionRepository; +import org.folio.spring.service.SystemUserScopedExecutionService; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; + +class EcsRequestTransactionsApiControllerTest extends BaseIT { + + private static final String TRANSACTION_AUDIT_DUPLICATE_ERROR_ACTION = "DUPLICATE_ERROR"; + private static final String DUPLICATE_ERROR_TRANSACTION_ID = "-1"; + + @Autowired + private TransactionRepository transactionRepository; + @Autowired + private TransactionAuditRepository transactionAuditRepository; + @Autowired + private SystemUserScopedExecutionService systemUserScopedExecutionService; + + @Test + void createLendingEcsRequestTest() throws Exception { + removeExistedTransactionFromDbIfSoExists(); + + this.mockMvc.perform( + post("/ecs-request-transactions/" + CIRCULATION_REQUEST_ID) + .content(asJsonString(createLendingEcsRequestTransactionByRole())) + .headers(defaultHeaders()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()); + + //Trying to create another transaction with same transaction id + this.mockMvc.perform( + post("/ecs-request-transactions/" + CIRCULATION_REQUEST_ID) + .content(asJsonString(createLendingEcsRequestTransactionByRole())) + .headers(defaultHeaders()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpectAll(status().is4xxClientError(), + jsonPath("$.errors[0].code", is("DUPLICATE_ERROR"))); + + // check for DUPLICATE_ERROR propagated into transactions_audit. + systemUserScopedExecutionService.executeAsyncSystemUserScoped( + TENANT, + () -> { + TransactionAuditEntity auditExisting = transactionAuditRepository + .findLatestTransactionAuditEntityByDcbTransactionId(CIRCULATION_REQUEST_ID) + .orElse(null); + Assertions.assertNotNull(auditExisting); + Assertions.assertNotEquals(TRANSACTION_AUDIT_DUPLICATE_ERROR_ACTION, + auditExisting.getAction()); + Assertions.assertNotEquals(DUPLICATE_ERROR_TRANSACTION_ID, + auditExisting.getTransactionId()); + } + ); + } + + @Test + void createBorrowingEcsRequestTest() throws Exception { + removeExistedTransactionFromDbIfSoExists(); + + this.mockMvc.perform( + post("/ecs-request-transactions/" + CIRCULATION_REQUEST_ID) + .content(asJsonString(createBorrowingEcsRequestTransactionByRole())) + .headers(defaultHeaders()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isCreated()); + } + + @Test + void checkErrorStatusForInvalidRequest() throws Exception { + DcbTransaction dcbTransaction = createLendingEcsRequestTransactionByRole(); + dcbTransaction.setRequestId(UUID.randomUUID().toString()); + this.mockMvc.perform( + post("/ecs-request-transactions/" + CIRCULATION_REQUEST_ID) + .content(asJsonString(dcbTransaction)) + .headers(defaultHeaders()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpectAll(status().is4xxClientError()); + } + + private void removeExistedTransactionFromDbIfSoExists() { + systemUserScopedExecutionService.executeAsyncSystemUserScoped(TENANT, () -> { + if (transactionRepository.existsById(CIRCULATION_REQUEST_ID)) { + transactionRepository.deleteById(CIRCULATION_REQUEST_ID); + } + }); + } +} diff --git a/src/test/java/org/folio/dcb/controller/TransactionApiControllerTest.java b/src/test/java/org/folio/dcb/controller/TransactionApiControllerTest.java index 029db24a..5edbc7ff 100644 --- a/src/test/java/org/folio/dcb/controller/TransactionApiControllerTest.java +++ b/src/test/java/org/folio/dcb/controller/TransactionApiControllerTest.java @@ -591,6 +591,26 @@ void transactionStatusUpdateFromCreatedToItemClosed() throws Exception { .andExpect(jsonPath("$.status").value("CLOSED")); } + @Test + void lenderTransactionStatusUpdateFromCreatedToOpen() throws Exception { + var transactionID = UUID.randomUUID().toString(); + var dcbTransaction = createTransactionEntity(); + dcbTransaction.setStatus(TransactionStatus.StatusEnum.CREATED); + dcbTransaction.setRole(LENDER); + dcbTransaction.setId(transactionID); + + systemUserScopedExecutionService.executeAsyncSystemUserScoped(TENANT, () -> transactionRepository.save(dcbTransaction)); + + this.mockMvc.perform( + put("/transactions/" + transactionID + "/status") + .content(asJsonString(createTransactionStatus(TransactionStatus.StatusEnum.OPEN))) + .headers(defaultHeaders()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("OPEN")); + } + @Test void transactionStatusUpdateFromItemCheckedOutToItemCheckedIn() throws Exception { var transactionID = UUID.randomUUID().toString(); diff --git a/src/test/java/org/folio/dcb/listener/CirculationRequestEventListenerTest.java b/src/test/java/org/folio/dcb/listener/CirculationRequestEventListenerTest.java index 37f1ccd1..ba2ac32f 100644 --- a/src/test/java/org/folio/dcb/listener/CirculationRequestEventListenerTest.java +++ b/src/test/java/org/folio/dcb/listener/CirculationRequestEventListenerTest.java @@ -91,7 +91,7 @@ void handleCancelRequestTest() { MessageHeaders messageHeaders = getMessageHeaders(); when(transactionRepository.findTransactionByRequestIdAndStatusNotInClosed(any())).thenReturn(Optional.of(transactionEntity)); eventListener.handleRequestEvent(REQUEST_CANCEL_EVENT_SAMPLE, messageHeaders); - Mockito.verify(transactionRepository, times(0)).save(any()); + Mockito.verify(transactionRepository, times(1)).save(any()); } @Test @@ -118,7 +118,7 @@ void handleOpenRequestTest() { when(circulationItemService.fetchItemById(anyString())).thenReturn(circulationItem); MessageHeaders messageHeaders = getMessageHeaders(); eventListener.handleRequestEvent(CHECK_IN_TRANSIT_EVENT_SAMPLE, messageHeaders); - Mockito.verify(transactionRepository, times(0)).save(any()); + Mockito.verify(transactionRepository, times(1)).save(any()); } @Test diff --git a/src/test/java/org/folio/dcb/service/CirculationServiceTest.java b/src/test/java/org/folio/dcb/service/CirculationServiceTest.java index 59c2a93f..eb461539 100644 --- a/src/test/java/org/folio/dcb/service/CirculationServiceTest.java +++ b/src/test/java/org/folio/dcb/service/CirculationServiceTest.java @@ -48,13 +48,13 @@ void checkInByBarcodeWithServicePointTest(){ void cancelRequestTest() { when(circulationRequestService.getCancellationRequestIfOpenOrNull(anyString())).thenReturn(createCirculationRequest()); circulationService.cancelRequest(createTransactionEntity()); - verify(circulationClient).cancelRequest(anyString(), any()); + verify(circulationClient).updateRequest(anyString(), any()); } @Test void shouldThrowExceptionWhenRequestIsNotUpdated() { when(circulationRequestService.getCancellationRequestIfOpenOrNull(anyString())).thenReturn(createCirculationRequest()); - when(circulationClient.cancelRequest(anyString(), any())).thenThrow(FeignException.BadRequest.class); + when(circulationClient.updateRequest(anyString(), any())).thenThrow(FeignException.BadRequest.class); assertThrows(CirculationRequestException.class, () -> circulationService.cancelRequest(createTransactionEntity())); } diff --git a/src/test/java/org/folio/dcb/service/StatusProcessorServiceTest.java b/src/test/java/org/folio/dcb/service/StatusProcessorServiceTest.java index 30a527a9..8b9d2bb1 100644 --- a/src/test/java/org/folio/dcb/service/StatusProcessorServiceTest.java +++ b/src/test/java/org/folio/dcb/service/StatusProcessorServiceTest.java @@ -45,14 +45,6 @@ void lendingChainProcessorTest() { @Test void lendingChainProcessorErrorTest() { - assertThrows(StatusException.class, () -> statusProcessorService.lendingChainProcessor(TransactionStatus.StatusEnum.CREATED, TransactionStatus.StatusEnum.OPEN)); - - assertThrows(StatusException.class, () -> statusProcessorService.lendingChainProcessor(TransactionStatus.StatusEnum.CREATED, TransactionStatus.StatusEnum.AWAITING_PICKUP)); - - assertThrows(StatusException.class, () -> statusProcessorService.lendingChainProcessor(TransactionStatus.StatusEnum.CREATED, TransactionStatus.StatusEnum.ITEM_CHECKED_OUT)); - - assertThrows(StatusException.class, () -> statusProcessorService.lendingChainProcessor(TransactionStatus.StatusEnum.CREATED, TransactionStatus.StatusEnum.ITEM_CHECKED_IN)); - assertThrows(StatusException.class, () -> statusProcessorService.lendingChainProcessor(TransactionStatus.StatusEnum.CREATED, TransactionStatus.StatusEnum.CLOSED)); assertThrows(StatusException.class, () -> statusProcessorService.lendingChainProcessor(TransactionStatus.StatusEnum.CREATED, TransactionStatus.StatusEnum.CREATED)); diff --git a/src/test/java/org/folio/dcb/service/TransactionAuditServiceTest.java b/src/test/java/org/folio/dcb/service/TransactionAuditServiceTest.java index 0f285daa..48606e31 100644 --- a/src/test/java/org/folio/dcb/service/TransactionAuditServiceTest.java +++ b/src/test/java/org/folio/dcb/service/TransactionAuditServiceTest.java @@ -15,7 +15,6 @@ import static org.folio.dcb.domain.dto.DcbTransaction.RoleEnum.LENDER; import static org.folio.dcb.utils.EntityUtils.DCB_TRANSACTION_ID; import static org.folio.dcb.utils.EntityUtils.createDcbTransactionByRole; -import static org.folio.dcb.utils.EntityUtils.createTransactionEntity; import static org.folio.dcb.utils.EntityUtils.createTransactionAuditEntity; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.times; @@ -41,11 +40,9 @@ void logTheErrorForExistedTransactionAuditTest() { } @Test void logTheErrorForNotExistedTransactionAuditTest() { - when(transactionMapper.mapToEntity(any(), any())).thenReturn(createTransactionEntity()); when(transactionAuditRepository.findLatestTransactionAuditEntityByDcbTransactionId(any())) .thenReturn(Optional.empty()); transactionAuditService.logErrorIfTransactionAuditNotExists(DCB_TRANSACTION_ID, createDcbTransactionByRole(LENDER), "error_message"); - Mockito.verify(transactionMapper, times(1)).mapToEntity(any(), any()); Mockito.verify(transactionAuditRepository, times(1)).save(any()); } diff --git a/src/test/java/org/folio/dcb/utils/EntityUtils.java b/src/test/java/org/folio/dcb/utils/EntityUtils.java index e27985c4..48d1b1e1 100644 --- a/src/test/java/org/folio/dcb/utils/EntityUtils.java +++ b/src/test/java/org/folio/dcb/utils/EntityUtils.java @@ -60,6 +60,7 @@ public class EntityUtils { public static String DCB_TYPE_USER_ID = "910c512c-ebc5-40c6-96a5-a20bfd81e154"; public static String EXISTED_INVENTORY_ITEM_BARCODE = "INVENTORY_ITEM"; public static String PATRON_TYPE_USER_ID = "18c1741d-e678-4c8e-9fe7-cfaeefab5eea"; + public static String REQUEST_ID = "398501a2-5c97-4ba6-9ee7-d1cd6433cb98"; public static DcbTransaction createDcbTransactionByRole(DcbTransaction.RoleEnum role) { return DcbTransaction.builder() @@ -74,6 +75,23 @@ public static DcbTransaction createDcbTransactionByRole(DcbTransaction.RoleEnum .build(); } + public static DcbTransaction createLendingEcsRequestTransactionByRole() { + return DcbTransaction.builder() + .requestId(REQUEST_ID) + .role(DcbTransaction.RoleEnum.LENDER) + .pickup(createDcbPickup()) + .build(); + } + + public static DcbTransaction createBorrowingEcsRequestTransactionByRole() { + return DcbTransaction.builder() + .requestId(REQUEST_ID) + .item(createDcbItem()) + .role(DcbTransaction.RoleEnum.BORROWER) + .pickup(createDcbPickup()) + .build(); + } + public static org.folio.dcb.domain.dto.ServicePointRequest createServicePointRequest() { return org.folio.dcb.domain.dto.ServicePointRequest.builder() .id(PICKUP_SERVICE_POINT_ID) diff --git a/src/test/resources/mappings/circulation.json b/src/test/resources/mappings/circulation.json index ac492f17..00ec8fc2 100644 --- a/src/test/resources/mappings/circulation.json +++ b/src/test/resources/mappings/circulation.json @@ -13,6 +13,18 @@ } } }, + { + "request": { + "method": "PUT", + "url": "/circulation/requests/398501a2-5c97-4ba6-9ee7-d1cd6433cb98" + }, + "response": { + "status": 204, + "headers": { + "Content-Type": "application/json" + } + } + }, { "request": { "method": "POST", diff --git a/src/test/resources/mappings/requests.json b/src/test/resources/mappings/requests.json new file mode 100644 index 00000000..c981dff1 --- /dev/null +++ b/src/test/resources/mappings/requests.json @@ -0,0 +1,29 @@ +{ + "mappings": [ + { + "request": { + "method": "GET", + "url": "/request-storage/requests/398501a2-5c97-4ba6-9ee7-d1cd6433cb98" + }, + "response": { + "status": 200, + "body": "{\n \"id\" : \"398501a2-5c97-4ba6-9ee7-d1cd6433cb98\",\n \"requestLevel\" : \"Item\",\n \"requestType\" : \"Page\",\n \"requestDate\" : \"2024-03-07T13:54:08.655+00:00\",\n \"requesterId\" : \"2205005b-ca51-4a04-87fd-938eefa8f6de\",\n \"instanceId\" : \"5bf370e0-8cca-4d9c-82e4-5170ab2a0a39\",\n \"holdingsRecordId\" : \"e3ff6133-b9a2-4d4c-a1c9-dc1867d4df19\",\n \"itemId\" : \"100d10bf-2f06-4aa0-be15-0b95b2d9f9e3\",\n \"status\" : \"Open - Not yet filled\",\n \"position\" : 1,\n \"instance\" : {\n \"title\" : \"A semantic web primer\",\n \"identifiers\" : [ {\n \"identifierTypeId\" : \"8261054f-be78-422d-bd51-4ed9f33c3422\",\n \"value\" : \"0262012103\"\n }, {\n \"identifierTypeId\" : \"8261054f-be78-422d-bd51-4ed9f33c3422\",\n \"value\" : \"9780262012102\"\n }, {\n \"identifierTypeId\" : \"c858e4f2-2b6b-4385-842b-60732ee14abb\",\n \"value\" : \"2003065165\"\n } ],\n \"contributorNames\" : [ {\n \"name\" : \"Antoniou, Grigoris\"\n }, {\n \"name\" : \"Van Harmelen, Frank\"\n } ],\n \"publication\" : [ {\n \"publisher\" : \"MIT Press\",\n \"place\" : \"Cambridge, Mass. \",\n \"dateOfPublication\" : \"c2004\",\n \"role\" : \"Publisher\"\n } ]\n },\n \"item\" : {\n \"barcode\" : \"90000\",\n \"location\" : {\n \"name\" : \"Annex\",\n \"libraryName\" : \"Datalogisk Institut\",\n \"code\" : \"KU/CC/DI/A\"\n },\n \"enumeration\" : \"\",\n \"status\" : \"Paged\",\n \"callNumber\" : \"TK5105.88815 . A58 2004 FT MEADE\",\n \"callNumberComponents\" : {\n \"callNumber\" : \"TK5105.88815 . A58 2004 FT MEADE\"\n }\n },\n \"requester\" : {\n \"lastName\" : \"rick\",\n \"firstName\" : \"psych\",\n \"barcode\" : \"123\",\n \"patronGroup\" : {\n \"id\" : \"3684a786-6671-4268-8ed0-9db82ebca60b\",\n \"group\" : \"staff\",\n \"desc\" : \"Staff Member\"\n },\n \"patronGroupId\" : \"3684a786-6671-4268-8ed0-9db82ebca60b\"\n },\n \"fulfillmentPreference\" : \"Hold Shelf\",\n \"pickupServicePointId\" : \"3a40852d-49fd-4df2-a1f9-6e2641a6e91f\",\n \"metadata\" : {\n \"createdDate\" : \"2024-03-07T13:54:13.484+00:00\",\n \"createdByUserId\" : \"5600bae3-4ca8-42dd-bef5-4502aaea6dc7\",\n \"updatedDate\" : \"2024-03-07T13:54:14.768+00:00\",\n \"updatedByUserId\" : \"5600bae3-4ca8-42dd-bef5-4502aaea6dc7\"\n },\n \"pickupServicePoint\" : {\n \"name\" : \"Circ Desk 1\",\n \"code\" : \"cd1\",\n \"discoveryDisplayName\" : \"Circulation Desk -- Hallway\",\n \"description\" : null,\n \"shelvingLagTime\" : null,\n \"pickupLocation\" : true\n }\n }", + "headers": { + "Content-Type": "application/json" + } + } + }, + { + "request": { + "method": "PUT", + "url": "/request-storage/requests/398501a2-5c97-4ba6-9ee7-d1cd6433cb98" + }, + "response": { + "status": 204, + "headers": { + "Content-Type": "application/json" + } + } + } + ] +}