diff --git a/src/main/java/org/folio/dcb/controller/ExceptionHandlingController.java b/src/main/java/org/folio/dcb/controller/ExceptionHandlingController.java index 88a2bc74..9ccfdef9 100644 --- a/src/main/java/org/folio/dcb/controller/ExceptionHandlingController.java +++ b/src/main/java/org/folio/dcb/controller/ExceptionHandlingController.java @@ -174,6 +174,6 @@ public ResponseEntity handleDcbHubLocationException(DcbHubLocationExcept } private void logExceptionMessage(Exception ex) { - log.warn("Exception occurred [{}]", ex.getClass().getSimpleName()); + log.warn("Exception occurred [{}]", ex.getClass().getSimpleName(), ex); } } diff --git a/src/main/java/org/folio/dcb/service/CirculationService.java b/src/main/java/org/folio/dcb/service/CirculationService.java index 0310bf99..4dac85ff 100644 --- a/src/main/java/org/folio/dcb/service/CirculationService.java +++ b/src/main/java/org/folio/dcb/service/CirculationService.java @@ -1,5 +1,6 @@ package org.folio.dcb.service; +import org.folio.dcb.domain.dto.ClaimedReturnedResolution; import org.folio.dcb.domain.entity.TransactionEntity; public interface CirculationService { @@ -13,6 +14,16 @@ public interface CirculationService { void checkInByBarcode(TransactionEntity dcbTransaction, String servicePointId); + /** + * Check in item by barcode with an optional claim returned resolution. + * + * @param dcbTransaction dcbTransactionEntity + * @param servicePointId service point id + * @param claimedReturnedResolution optional claim returned resolution + */ + void checkInByBarcode(TransactionEntity dcbTransaction, String servicePointId, + ClaimedReturnedResolution claimedReturnedResolution); + /** * Check out an item by barcode. * diff --git a/src/main/java/org/folio/dcb/service/impl/BorrowingLibraryServiceImpl.java b/src/main/java/org/folio/dcb/service/impl/BorrowingLibraryServiceImpl.java index 9b964396..d8d0d7e6 100644 --- a/src/main/java/org/folio/dcb/service/impl/BorrowingLibraryServiceImpl.java +++ b/src/main/java/org/folio/dcb/service/impl/BorrowingLibraryServiceImpl.java @@ -9,12 +9,14 @@ import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.ITEM_CHECKED_OUT; import static org.folio.dcb.domain.dto.TransactionStatus.StatusEnum.OPEN; +import java.util.Optional; import java.util.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.folio.dcb.domain.dto.DcbTransaction; import org.folio.dcb.domain.dto.ServicePointRequest; import org.folio.dcb.domain.dto.TransactionStatus; +import org.folio.dcb.domain.dto.TransactionStatusContext; import org.folio.dcb.domain.dto.TransactionStatusResponse; import org.folio.dcb.domain.entity.TransactionEntity; import org.folio.dcb.repository.TransactionRepository; @@ -47,11 +49,20 @@ public void updateTransactionStatus(TransactionEntity dcbTransaction, Transactio dcbTransaction.getId(), dcbTransaction.getStatus(), transactionStatus.getStatus()); var currStatus = dcbTransaction.getStatus(); var newStatus = transactionStatus.getStatus(); + String randomServicePointId = UUID.randomUUID().toString(); - if (CREATED == currStatus && OPEN == newStatus || ITEM_CHECKED_OUT == currStatus && ITEM_CHECKED_IN == newStatus) { + if (CREATED == currStatus && OPEN == newStatus) { log.info("updateTransactionStatus:: Checking in item for transaction {}.", dcbTransaction.getId()); - // Random UUID for servicePointId. - circulationService.checkInByBarcode(dcbTransaction, UUID.randomUUID().toString()); + circulationService.checkInByBarcode(dcbTransaction, randomServicePointId); + updateTransactionEntity(dcbTransaction, newStatus); + } else if (ITEM_CHECKED_OUT == currStatus && ITEM_CHECKED_IN == newStatus) { + log.info("updateTransactionStatus:: Checking in item for transaction {}.", dcbTransaction.getId()); + Optional.ofNullable(transactionStatus.getContext()) + .map(TransactionStatusContext::getClaimedReturnedResolution) + .ifPresentOrElse( + resolution -> circulationService.checkInByBarcode(dcbTransaction, randomServicePointId, resolution), + () -> circulationService.checkInByBarcode(dcbTransaction, randomServicePointId)); + updateTransactionEntity(dcbTransaction, newStatus); } else if (OPEN == currStatus && AWAITING_PICKUP == newStatus) { circulationService.checkInByBarcode(dcbTransaction); 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 22e26a1e..c6f663ee 100644 --- a/src/main/java/org/folio/dcb/service/impl/CirculationServiceImpl.java +++ b/src/main/java/org/folio/dcb/service/impl/CirculationServiceImpl.java @@ -5,6 +5,7 @@ import lombok.extern.log4j.Log4j2; import org.folio.dcb.domain.dto.CheckInRequest; import org.folio.dcb.domain.dto.CheckOutRequest; +import org.folio.dcb.domain.dto.ClaimedReturnedResolution; import org.folio.dcb.domain.entity.TransactionEntity; import org.folio.dcb.exception.CirculationRequestException; import org.folio.dcb.integration.circulation.CirculationClient; @@ -34,6 +35,16 @@ public void checkInByBarcode(TransactionEntity dcbTransaction, String servicePoi circulationClient.checkInByBarcode(createCheckInRequest(dcbTransaction.getItemBarcode(), servicePointId)); } + @Override + public void checkInByBarcode(TransactionEntity dcbTransaction, String servicePointId, + ClaimedReturnedResolution claimedReturnedResolution) { + + log.info("checkInByBarcode:: checking in item for transaction {} with claimedReturnedResolution '{}'.", + dcbTransaction.getId(), claimedReturnedResolution); + circulationClient.checkInByBarcode(createCheckInRequest(dcbTransaction.getItemBarcode(), servicePointId, + claimedReturnedResolution)); + } + @Override public void checkOutByBarcode(TransactionEntity dcbTransaction) { log.debug("checkOutByBarcode:: checking out item for transaction {}.", dcbTransaction.getId()); @@ -60,10 +71,17 @@ public void cancelRequest(TransactionEntity dcbTransaction, boolean isItemUnavai } private CheckInRequest createCheckInRequest(String itemBarcode, String servicePointId) { + return createCheckInRequest(itemBarcode, servicePointId, null); + } + + private CheckInRequest createCheckInRequest(String itemBarcode, String servicePointId, + ClaimedReturnedResolution claimedReturnedResolution) { + return CheckInRequest.builder() .itemBarcode(itemBarcode) .servicePointId(servicePointId) .checkInDate(OffsetDateTime.now().toString()) + .claimedReturnedResolution(claimedReturnedResolution) .build(); } 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 693eaef2..e0f81df0 100644 --- a/src/main/java/org/folio/dcb/service/impl/TransactionsServiceImpl.java +++ b/src/main/java/org/folio/dcb/service/impl/TransactionsServiceImpl.java @@ -229,8 +229,8 @@ private void processLenderRoleTransaction(TransactionStatus status, TransactionE private void processBorrowerTransaction(TransactionStatus status, TransactionEntity transaction) { statusProcessorService.borrowingChainProcessor(transaction.getStatus(), status.getStatus()) - .forEach(statusEnum -> borrowingLibraryService.updateTransactionStatus( - transaction, TransactionStatus.builder().status(statusEnum).build())); + .forEach(statusEnum -> borrowingLibraryService.updateTransactionStatus(transaction, + TransactionStatus.builder().status(statusEnum).context(status.getContext()).build())); } private void validateLoanPolicy(String loanPolicyId, LoanPolicy loanPolicy) { diff --git a/src/main/resources/swagger.api/dcb_transaction.yaml b/src/main/resources/swagger.api/dcb_transaction.yaml index 8fd22d5d..32830538 100644 --- a/src/main/resources/swagger.api/dcb_transaction.yaml +++ b/src/main/resources/swagger.api/dcb_transaction.yaml @@ -502,6 +502,8 @@ components: $ref: 'schemas/CirculationRequest.yaml#/CirculationRequest' CheckInRequest: $ref: 'schemas/CheckInRequest.yaml#/CheckInRequest' + ClaimedReturnedResolution: + $ref: 'schemas/claimedReturnedResolution.yaml#/ClaimedReturnedResolution' CheckOutRequest: $ref: 'schemas/checkOutRequest.yaml#/CheckOutRequest' ServicePointRequest: diff --git a/src/main/resources/swagger.api/schemas/CheckInRequest.yaml b/src/main/resources/swagger.api/schemas/CheckInRequest.yaml index 32e555a5..4ad48968 100644 --- a/src/main/resources/swagger.api/schemas/CheckInRequest.yaml +++ b/src/main/resources/swagger.api/schemas/CheckInRequest.yaml @@ -13,3 +13,5 @@ CheckInRequest: description: When the item was checked in type: string format: date-type + claimedReturnedResolution: + $ref: 'claimedReturnedResolution.yaml#/ClaimedReturnedResolution' diff --git a/src/main/resources/swagger.api/schemas/claimedReturnedResolution.yaml b/src/main/resources/swagger.api/schemas/claimedReturnedResolution.yaml new file mode 100644 index 00000000..780ce1db --- /dev/null +++ b/src/main/resources/swagger.api/schemas/claimedReturnedResolution.yaml @@ -0,0 +1,6 @@ +ClaimedReturnedResolution: + type: string + description: Claimed returned resolution + enum: + - Found by library + - Returned by patron diff --git a/src/main/resources/swagger.api/schemas/transactionStatus.yaml b/src/main/resources/swagger.api/schemas/transactionStatus.yaml index 207b9b36..6dc9423c 100644 --- a/src/main/resources/swagger.api/schemas/transactionStatus.yaml +++ b/src/main/resources/swagger.api/schemas/transactionStatus.yaml @@ -15,3 +15,9 @@ TransactionStatus: - ERROR #Error occurred message: type: string + context: + type: object + description: Optional context for the transaction status update + properties: + claimedReturnedResolution: + $ref: 'claimedReturnedResolution.yaml#/ClaimedReturnedResolution' diff --git a/src/test/java/org/folio/dcb/it/BorrowerTransactionIT.java b/src/test/java/org/folio/dcb/it/BorrowerTransactionIT.java index 1e332eaa..d99fbb62 100644 --- a/src/test/java/org/folio/dcb/it/BorrowerTransactionIT.java +++ b/src/test/java/org/folio/dcb/it/BorrowerTransactionIT.java @@ -43,7 +43,10 @@ import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.util.Map; +import org.folio.dcb.domain.dto.ClaimedReturnedResolution; +import org.folio.dcb.domain.dto.TransactionStatus; import org.folio.dcb.domain.dto.TransactionStatus.StatusEnum; +import org.folio.dcb.domain.dto.TransactionStatusContext; import org.folio.dcb.it.base.BaseTenantIntegrationTest; import org.folio.dcb.support.types.IntegrationTest; import org.folio.dcb.support.wiremock.WireMockStub; @@ -221,6 +224,54 @@ void updateTransactionStatus_positive_fromItemCheckedOutToItemCheckedIn() throws .andExpect(jsonPath("$.status").value("ITEM_CHECKED_IN")); } + @Test + @WireMockStub("/stubs/mod-circulation/check-in-by-barcode/201-post(with claim returned - found by library).json") + void updateTransactionStatus_positive_fromItemCheckedOutToItemCheckedInWithFoundByLibrary() throws Exception { + testJdbcHelper.saveDcbTransaction(DCB_TRANSACTION_ID, ITEM_CHECKED_OUT, borrowerDcbTransaction()); + var context = TransactionStatusContext.builder() + .claimedReturnedResolution(ClaimedReturnedResolution.FOUND_BY_LIBRARY) + .build(); + var transactionStatus = TransactionStatus.builder() + .status(ITEM_CHECKED_IN) + .context(context) + .build(); + putDcbTransactionStatus(DCB_TRANSACTION_ID, transactionStatus) + .andExpect(jsonPath("$.status").value("ITEM_CHECKED_IN")); + + wiremock.verifyThat(1, postRequestedFor(urlPathEqualTo("/circulation/check-in-by-barcode")) + .withRequestBody(matchingJsonPath("$.claimedReturnedResolution", equalTo("Found by library")))); + } + + @Test + @WireMockStub("/stubs/mod-circulation/check-in-by-barcode/201-post(with claim returned - returned by patron).json") + void updateTransactionStatus_positive_fromItemCheckedOutToItemCheckedInWithReturnedByPatron() throws Exception { + + testJdbcHelper.saveDcbTransaction(DCB_TRANSACTION_ID, ITEM_CHECKED_OUT, borrowerDcbTransaction()); + var context = TransactionStatusContext.builder() + .claimedReturnedResolution(ClaimedReturnedResolution.RETURNED_BY_PATRON) + .build(); + var transactionStatus = TransactionStatus.builder() + .status(ITEM_CHECKED_IN) + .context(context) + .build(); + putDcbTransactionStatus(DCB_TRANSACTION_ID, transactionStatus) + .andExpect(jsonPath("$.status").value("ITEM_CHECKED_IN")); + + wiremock.verifyThat(1, postRequestedFor(urlPathEqualTo("/circulation/check-in-by-barcode")) + .withRequestBody(matchingJsonPath("$.claimedReturnedResolution", equalTo("Returned by patron")))); + } + + @Test + void updateTransactionStatus_negative_invalidClaimedReturnedResolution() throws Exception { + testJdbcHelper.saveDcbTransaction(DCB_TRANSACTION_ID, ITEM_CHECKED_OUT, borrowerDcbTransaction()); + mockMvc.perform(put("/transactions/{id}/status", DCB_TRANSACTION_ID) + .content("{\"status\":\"ITEM_CHECKED_IN\",\"context\":{\"claimedReturnedResolution\":\"INVALID_VALUE\"}}") + .headers(defaultHeaders()) + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()); + } + @Test @WireMockStub({ "/stubs/mod-circulation/check-in-by-barcode/201-post(random SP).json", diff --git a/src/test/java/org/folio/dcb/service/BorrowingLibraryServiceTest.java b/src/test/java/org/folio/dcb/service/BorrowingLibraryServiceTest.java index a3ab3500..248eed46 100644 --- a/src/test/java/org/folio/dcb/service/BorrowingLibraryServiceTest.java +++ b/src/test/java/org/folio/dcb/service/BorrowingLibraryServiceTest.java @@ -14,12 +14,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.UUID; +import org.folio.dcb.domain.dto.ClaimedReturnedResolution; import org.folio.dcb.domain.dto.TransactionStatus; +import org.folio.dcb.domain.dto.TransactionStatusContext; import org.folio.dcb.repository.TransactionRepository; import org.folio.dcb.service.impl.BaseLibraryService; import org.folio.dcb.service.impl.BorrowingLibraryServiceImpl; @@ -116,4 +119,40 @@ void testTransactionStatusUpdateFromItemCheckedOutToItemCheckedIn() { verify(circulationService).checkInByBarcode(any(), any()); assertEquals(ITEM_CHECKED_IN, transactionEntity.getStatus()); } + + @Test + void testTransactionStatusUpdateFromItemCheckedOutToItemCheckedInWithFoundByLibrary() { + var transactionEntity = createTransactionEntity(); + transactionEntity.setStatus(ITEM_CHECKED_OUT); + doNothing().when(circulationService).checkInByBarcode(any(), any(), any()); + var context = TransactionStatusContext.builder() + .claimedReturnedResolution(ClaimedReturnedResolution.FOUND_BY_LIBRARY) + .build(); + TransactionStatus transactionStatus = TransactionStatus.builder() + .status(ITEM_CHECKED_IN) + .context(context) + .build(); + borrowingLibraryService.updateTransactionStatus(transactionEntity, transactionStatus); + + verify(circulationService).checkInByBarcode(any(), any(), eq(ClaimedReturnedResolution.FOUND_BY_LIBRARY)); + assertEquals(ITEM_CHECKED_IN, transactionEntity.getStatus()); + } + + @Test + void testTransactionStatusUpdateFromItemCheckedOutToItemCheckedInWithReturnedByPatron() { + var transactionEntity = createTransactionEntity(); + transactionEntity.setStatus(ITEM_CHECKED_OUT); + doNothing().when(circulationService).checkInByBarcode(any(), any(), any()); + var context = TransactionStatusContext.builder() + .claimedReturnedResolution(ClaimedReturnedResolution.RETURNED_BY_PATRON) + .build(); + TransactionStatus transactionStatus = TransactionStatus.builder() + .status(ITEM_CHECKED_IN) + .context(context) + .build(); + borrowingLibraryService.updateTransactionStatus(transactionEntity, transactionStatus); + + verify(circulationService).checkInByBarcode(any(), any(), eq(ClaimedReturnedResolution.RETURNED_BY_PATRON)); + assertEquals(ITEM_CHECKED_IN, transactionEntity.getStatus()); + } } diff --git a/src/test/java/org/folio/dcb/service/CirculationServiceTest.java b/src/test/java/org/folio/dcb/service/CirculationServiceTest.java index 28693498..332785d6 100644 --- a/src/test/java/org/folio/dcb/service/CirculationServiceTest.java +++ b/src/test/java/org/folio/dcb/service/CirculationServiceTest.java @@ -1,15 +1,18 @@ package org.folio.dcb.service; +import static java.util.UUID.randomUUID; import static org.folio.dcb.utils.EntityUtils.createCirculationRequest; import static org.folio.dcb.utils.EntityUtils.createTransactionEntity; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.util.UUID; import org.folio.dcb.domain.dto.CirculationRequest; +import org.folio.dcb.domain.dto.ClaimedReturnedResolution; import org.folio.dcb.domain.entity.TransactionEntity; import org.folio.dcb.exception.CirculationRequestException; import org.folio.dcb.integration.circulation.CirculationClient; @@ -42,6 +45,20 @@ void checkInByBarcodeWithServicePointTest() { verify(circulationClient).checkInByBarcode(any()); } + @Test + void checkInByBarcodeWithServicePointAndClaimedReturnedResolutionTest() { + circulationService.checkInByBarcode(createTransactionEntity(), randomUUID().toString(), + ClaimedReturnedResolution.FOUND_BY_LIBRARY); + verify(circulationClient).checkInByBarcode(argThat(req -> + ClaimedReturnedResolution.FOUND_BY_LIBRARY.equals(req.getClaimedReturnedResolution()))); + } + + @Test + void checkInByBarcodeWithServicePointAndNullClaimedReturnedResolutionTest() { + circulationService.checkInByBarcode(createTransactionEntity(), randomUUID().toString(), null); + verify(circulationClient).checkInByBarcode(argThat(req -> req.getClaimedReturnedResolution() == null)); + } + @Test void shouldUpdateRequestApiWithIsDcbRerequestCancellationTrue() { CirculationRequest fetchedRequest = createCirculationRequest(); diff --git a/src/test/resources/stubs/mod-circulation/check-in-by-barcode/201-post(with claim returned - found by library).json b/src/test/resources/stubs/mod-circulation/check-in-by-barcode/201-post(with claim returned - found by library).json new file mode 100644 index 00000000..294f3b22 --- /dev/null +++ b/src/test/resources/stubs/mod-circulation/check-in-by-barcode/201-post(with claim returned - found by library).json @@ -0,0 +1,54 @@ +{ + "request": { + "method": "POST", + "urlPath": "/circulation/check-in-by-barcode", + "bodyPatterns": [ + { + "equalToJson": { + "itemBarcode": "DCB_ITEM", + "claimedReturnedResolution": "Found by library" + }, + "ignoreExtraElements": true + }, + { "matchesJsonPath": "$.checkInDate" }, + { + "matchesJsonPath": { + "expression": "$.servicePointId", + "matches": "[0-9a-f-]{36}" + } + } + ], + "headers": { + "x-okapi-tenant": { + "equalTo": "test_tenant" + } + } + }, + "response": { + "status": 201, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "loan": { + "id": "cf23adf0-61ba-4887-bf82-956c4aae2260", + "userId": "df7f4993-8c14-4a0f-ab63-93975ab01c76", + "itemId": "5b95877d-86c0-4cb7-a0cd-7660b348ae5a", + "item": { + "title": "The Long Way to a Small, Angry Planet", + "barcode": "DCB_ITEM", + "status": { + "name": "Checked out" + } + }, + "loanDate": "2017-03-01T23:11:00.000Z", + "dueDate": "2017-04-01T23:11:00.000Z", + "checkinServicePointId": "9d1b77e8-f02e-4b7f-b296-3f2042ddac55", + "status": { + "name": "Closed" + }, + "action": "checkedin" + } + } + } +} diff --git a/src/test/resources/stubs/mod-circulation/check-in-by-barcode/201-post(with claim returned - returned by patron).json b/src/test/resources/stubs/mod-circulation/check-in-by-barcode/201-post(with claim returned - returned by patron).json new file mode 100644 index 00000000..ef5f718e --- /dev/null +++ b/src/test/resources/stubs/mod-circulation/check-in-by-barcode/201-post(with claim returned - returned by patron).json @@ -0,0 +1,54 @@ +{ + "request": { + "method": "POST", + "urlPath": "/circulation/check-in-by-barcode", + "bodyPatterns": [ + { + "equalToJson": { + "itemBarcode": "DCB_ITEM", + "claimedReturnedResolution": "Returned by patron" + }, + "ignoreExtraElements": true + }, + { "matchesJsonPath": "$.checkInDate" }, + { + "matchesJsonPath": { + "expression": "$.servicePointId", + "matches": "[0-9a-f-]{36}" + } + } + ], + "headers": { + "x-okapi-tenant": { + "equalTo": "test_tenant" + } + } + }, + "response": { + "status": 201, + "headers": { + "Content-Type": "application/json" + }, + "jsonBody": { + "loan": { + "id": "cf23adf0-61ba-4887-bf82-956c4aae2260", + "userId": "df7f4993-8c14-4a0f-ab63-93975ab01c76", + "itemId": "5b95877d-86c0-4cb7-a0cd-7660b348ae5a", + "item": { + "title": "The Long Way to a Small, Angry Planet", + "barcode": "DCB_ITEM", + "status": { + "name": "Checked out" + } + }, + "loanDate": "2017-03-01T23:11:00.000Z", + "dueDate": "2017-04-01T23:11:00.000Z", + "checkinServicePointId": "9d1b77e8-f02e-4b7f-b296-3f2042ddac55", + "status": { + "name": "Closed" + }, + "action": "checkedin" + } + } + } +}