diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index 4da59f87..8072b311 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -124,7 +124,28 @@ "permissionsRequired": ["tlr.pick-slips.collection.get"], "modulePermissions": [ "user-tenants.collection.get", - "search.instances.collection.get", + "circulation-storage.requests.item.get", + "circulation-storage.requests.collection.get", + "users.item.get", + "users.collection.get", + "usergroups.item.get", + "usergroups.collection.get", + "departments.item.get", + "departments.collection.get", + "addresstypes.item.get", + "addresstypes.collection.get", + "inventory-storage.service-points.item.get", + "inventory-storage.service-points.collection.get", + "inventory-storage.instances.item.get", + "inventory-storage.instances.collection.get" + ] + }, + { + "methods": ["GET"], + "pathPattern": "/tlr/staff-slips/search-slips/{servicePointId}", + "permissionsRequired": ["tlr.search-slips.collection.get"], + "modulePermissions": [ + "user-tenants.collection.get", "circulation-storage.requests.item.get", "circulation-storage.requests.collection.get", "users.item.get", diff --git a/src/main/java/org/folio/listener/kafka/KafkaEventListener.java b/src/main/java/org/folio/listener/kafka/KafkaEventListener.java index f6e7b840..dc0f9a05 100644 --- a/src/main/java/org/folio/listener/kafka/KafkaEventListener.java +++ b/src/main/java/org/folio/listener/kafka/KafkaEventListener.java @@ -5,12 +5,14 @@ import java.nio.charset.StandardCharsets; import java.util.Optional; +import org.folio.domain.dto.Loan; import org.folio.domain.dto.Request; import org.folio.domain.dto.RequestsBatchUpdate; import org.folio.domain.dto.User; import org.folio.domain.dto.UserGroup; import org.folio.exception.KafkaEventDeserializationException; import org.folio.service.KafkaEventHandler; +import org.folio.service.impl.LoanEventHandler; import org.folio.service.impl.RequestBatchUpdateEventHandler; import org.folio.service.impl.RequestEventHandler; import org.folio.service.impl.UserEventHandler; @@ -34,18 +36,22 @@ public class KafkaEventListener { private static final ObjectMapper objectMapper = new ObjectMapper(); private final RequestEventHandler requestEventHandler; + private final LoanEventHandler loanEventHandler; private final UserGroupEventHandler userGroupEventHandler; private final UserEventHandler userEventHandler; private final SystemUserScopedExecutionService systemUserScopedExecutionService; private final RequestBatchUpdateEventHandler requestBatchEventHandler; - public KafkaEventListener(@Autowired RequestEventHandler requestEventHandler, - @Autowired RequestBatchUpdateEventHandler requestBatchEventHandler, - @Autowired SystemUserScopedExecutionService systemUserScopedExecutionService, - @Autowired UserGroupEventHandler userGroupEventHandler, - @Autowired UserEventHandler userEventHandler) { + @Autowired + public KafkaEventListener(RequestEventHandler requestEventHandler, + LoanEventHandler loanEventHandler, + RequestBatchUpdateEventHandler requestBatchEventHandler, + SystemUserScopedExecutionService systemUserScopedExecutionService, + UserGroupEventHandler userGroupEventHandler, + UserEventHandler userEventHandler) { this.requestEventHandler = requestEventHandler; + this.loanEventHandler = loanEventHandler; this.systemUserScopedExecutionService = systemUserScopedExecutionService; this.userGroupEventHandler = userGroupEventHandler; this.requestBatchEventHandler = requestBatchEventHandler; @@ -76,6 +82,19 @@ public void handleRequestBatchUpdateEvent(String eventString, MessageHeaders mes log.info("handleRequestBatchUpdateEvent:: event consumed: {}", event::getId); } + @KafkaListener( + topicPattern = "${folio.environment}\\.\\w+\\.circulation\\.loan", + groupId = "${spring.kafka.consumer.group-id}" + ) + public void handleLoanEvent(String eventString, MessageHeaders messageHeaders) { + log.debug("handleLoanEvent:: event: {}", () -> eventString); + KafkaEvent event = deserialize(eventString, messageHeaders, Loan.class); + log.info("handleLoanEvent:: event received: {}", event::getId); + log.info("handleLoanEvent:: event: {}", eventString); + handleEvent(event, loanEventHandler); + log.info("handleLoanEvent:: event consumed: {}", event::getId); + } + private void handleEvent(KafkaEvent event, KafkaEventHandler handler) { try { systemUserScopedExecutionService.executeAsyncSystemUserScoped(CENTRAL_TENANT_ID, diff --git a/src/main/java/org/folio/service/impl/AllowedServicePointsServiceImpl.java b/src/main/java/org/folio/service/impl/AllowedServicePointsServiceImpl.java index 8ff2866f..f93cc6f9 100644 --- a/src/main/java/org/folio/service/impl/AllowedServicePointsServiceImpl.java +++ b/src/main/java/org/folio/service/impl/AllowedServicePointsServiceImpl.java @@ -62,8 +62,6 @@ private AllowedServicePointsResponse getForCreate(AllowedServicePointsRequest re Map recall = new HashMap<>(); for (String tenantId : getLendingTenants(request)) { var servicePoints = getAllowedServicePointsFromTenant(request, patronGroupId, tenantId); - log.info("getForCreate:: service points from {}: {}", tenantId, servicePoints); - combineAndFilterDuplicates(page, servicePoints.getPage()); combineAndFilterDuplicates(hold, servicePoints.getHold()); combineAndFilterDuplicates(recall, servicePoints.getRecall()); diff --git a/src/main/java/org/folio/service/impl/LoanEventHandler.java b/src/main/java/org/folio/service/impl/LoanEventHandler.java new file mode 100644 index 00000000..862aef39 --- /dev/null +++ b/src/main/java/org/folio/service/impl/LoanEventHandler.java @@ -0,0 +1,281 @@ +package org.folio.service.impl; + +import static org.folio.domain.dto.Request.EcsRequestPhaseEnum.PRIMARY; +import static org.folio.domain.dto.Request.EcsRequestPhaseEnum.SECONDARY; +import static org.folio.domain.dto.TransactionStatus.StatusEnum.AWAITING_PICKUP; +import static org.folio.domain.dto.TransactionStatus.StatusEnum.CANCELLED; +import static org.folio.domain.dto.TransactionStatus.StatusEnum.ITEM_CHECKED_OUT; +import static org.folio.domain.dto.TransactionStatus.StatusEnum.OPEN; +import static org.folio.support.KafkaEvent.EventType.UPDATED; + +import java.util.Date; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; +import java.util.function.Function; + +import org.folio.domain.dto.Loan; +import org.folio.domain.dto.Request; +import org.folio.domain.dto.Request.EcsRequestPhaseEnum; +import org.folio.domain.dto.Request.FulfillmentPreferenceEnum; +import org.folio.domain.dto.ServicePoint; +import org.folio.domain.dto.TransactionStatus; +import org.folio.domain.entity.EcsTlrEntity; +import org.folio.repository.EcsTlrRepository; +import org.folio.service.CloningService; +import org.folio.service.DcbService; +import org.folio.service.KafkaEventHandler; +import org.folio.service.RequestService; +import org.folio.service.ServicePointService; +import org.folio.spring.service.SystemUserScopedExecutionService; +import org.folio.support.KafkaEvent; +import org.springframework.stereotype.Service; + +import feign.FeignException; +import lombok.AllArgsConstructor; +import lombok.extern.log4j.Log4j2; + +@AllArgsConstructor +@Service +@Log4j2 +public class LoanEventHandler implements KafkaEventHandler { + + private final DcbService dcbService; + private final EcsTlrRepository ecsTlrRepository; + private final SystemUserScopedExecutionService executionService; + private final ServicePointService servicePointService; + private final CloningService servicePointCloningService; + private final RequestService requestService; + + @Override + public void handle(KafkaEvent event) { + log.info("handle:: processing loan event: {}", event::getId); + if (event.getType() == UPDATED) { + handleLoanUpdateEvent(event); + } else { + log.info("handle:: ignoring event {} of unsupported type: {}", event::getId, event::getType); + } + log.info("handle:: loan event processed: {}", event::getId); + } + + private void handleLoanUpdateEvent(KafkaEvent event) { + log.info("handleLoanUpdateEvent:: handling loan update event: {}", event::getId); +// Loan updatedRequest = event.getData().getNewVersion(); +// if (updatedRequest == null) { +// log.warn("handleRequestUpdateEvent:: event does not contain new version of request"); +// return; +// } +// if (updatedRequest.getEcsRequestPhase() == null) { +// log.info("handleRequestUpdateEvent:: updated request is not an ECS request"); +// return; +// } +// if (updatedRequest.getEcsRequestPhase() == SECONDARY && updatedRequest.getItemId() == null) { +// log.info("handleRequestUpdateEvent:: updated secondary request does not contain itemId"); +// return; +// } +// String requestId = updatedRequest.getId(); +// log.info("handleRequestUpdateEvent:: looking for ECS TLR for request {}", requestId); +// // we can search by either primary or secondary request ID, they are identical +// ecsTlrRepository.findBySecondaryRequestId(UUID.fromString(requestId)).ifPresentOrElse( +// ecsTlr -> handleRequestUpdateEvent(ecsTlr, event), +// () -> log.info("handleSecondaryRequestUpdate: ECS TLR for request {} not found", requestId)); + } + + private void handleRequestUpdateEvent(EcsTlrEntity ecsTlr, KafkaEvent event) { + log.debug("handleRequestUpdateEvent:: ecsTlr={}", () -> ecsTlr); + Request updatedRequest = event.getData().getNewVersion(); + + if (!requestMatchesEcsTlr(ecsTlr, updatedRequest, event.getTenantIdHeaderValue())) { + return; + } + if (updatedRequest.getEcsRequestPhase() == PRIMARY) { + handlePrimaryRequestUpdate(ecsTlr, event); + } + if (updatedRequest.getEcsRequestPhase() == SECONDARY) { + handleSecondaryRequestUpdate(ecsTlr, event); + } + } + + private static boolean requestMatchesEcsTlr(EcsTlrEntity ecsTlr, Request updatedRequest, + String updatedRequestTenant) { + + final EcsRequestPhaseEnum updatedRequestPhase = updatedRequest.getEcsRequestPhase(); + final UUID updatedRequestId = UUID.fromString(updatedRequest.getId()); + + if (updatedRequestPhase == PRIMARY && updatedRequestId.equals(ecsTlr.getPrimaryRequestId()) + && updatedRequestTenant.equals(ecsTlr.getPrimaryRequestTenantId())) { + log.info("requestMatchesEcsTlr:: updated primary request matches ECS TLR"); + return true; + } else if (updatedRequestPhase == SECONDARY && updatedRequestId.equals(ecsTlr.getSecondaryRequestId()) + && updatedRequestTenant.equals(ecsTlr.getSecondaryRequestTenantId())) { + log.info("requestMatchesEcsTlr:: updated secondary request matches ECS TLR"); + return true; + } + log.warn("requestMatchesEcsTlr:: request does not match ECS TLR: updatedRequestPhase={}, " + + "updatedRequestId={}, updatedRequestTenant={}, ecsTlr={}", updatedRequestPhase, + updatedRequestId, updatedRequestTenant, ecsTlr); + return false; + } + + private void handlePrimaryRequestUpdate(EcsTlrEntity ecsTlr, KafkaEvent event) { + propagateChangesFromPrimaryToSecondaryRequest(ecsTlr, event); + updateTransactionStatuses(event, ecsTlr); + } + + private void handleSecondaryRequestUpdate(EcsTlrEntity ecsTlr, KafkaEvent event) { + processItemIdUpdate(ecsTlr, event.getData().getNewVersion()); + updateTransactionStatuses(event, ecsTlr); + } + + private void processItemIdUpdate(EcsTlrEntity ecsTlr, Request updatedRequest) { + if (ecsTlr.getItemId() != null) { + log.info("processItemIdUpdate:: ECS TLR {} already has itemId {}", ecsTlr::getId, ecsTlr::getItemId); + return; + } + log.info("processItemIdUpdate:: updating ECS TLR {} with itemId {}", ecsTlr::getId, + updatedRequest::getItemId); + ecsTlr.setItemId(UUID.fromString(updatedRequest.getItemId())); + // TODO: change this if Page request works + dcbService.createTransactions(ecsTlr, updatedRequest); + ecsTlrRepository.save(ecsTlr); + log.info("processItemIdUpdate: ECS TLR {} is updated", ecsTlr::getId); + } + + private static Optional determineNewTransactionStatus( + KafkaEvent event) { + + final Request.StatusEnum oldRequestStatus = event.getData().getOldVersion().getStatus(); + final Request.StatusEnum newRequestStatus = event.getData().getNewVersion().getStatus(); + log.info("determineNewTransactionStatus:: oldRequestStatus='{}', newRequestStatus='{}'", + oldRequestStatus, newRequestStatus); + + if (newRequestStatus == oldRequestStatus) { + log.info("determineNewTransactionStatus:: request status did not change"); + return Optional.empty(); + } + + var newTransactionStatus = Optional.ofNullable( + switch (newRequestStatus) { + case OPEN_IN_TRANSIT -> OPEN; + case OPEN_AWAITING_PICKUP -> AWAITING_PICKUP; + case CLOSED_FILLED -> ITEM_CHECKED_OUT; + case CLOSED_CANCELLED -> CANCELLED; + default -> null; + }); + + newTransactionStatus.ifPresentOrElse( + ts -> log.info("determineNewTransactionStatus:: new transaction status: {}", ts), + () -> log.info("determineNewTransactionStatus:: irrelevant request status change")); + + return newTransactionStatus; + } + + private void updateTransactionStatuses(KafkaEvent event, EcsTlrEntity ecsTlr) { + determineNewTransactionStatus(event) + .ifPresent(newStatus -> updateTransactionStatuses(newStatus, ecsTlr)); + } + + private void updateTransactionStatuses(TransactionStatus.StatusEnum newStatus, EcsTlrEntity ecsTlr) { + log.info("updateTransactionStatuses:: updating primary transaction status to {}", newStatus::getValue); + updateTransactionStatus(ecsTlr.getPrimaryRequestDcbTransactionId(), newStatus, + ecsTlr.getPrimaryRequestTenantId()); + + log.info("updateTransactionStatuses:: updating intermediate transaction status to {}", newStatus::getValue); + updateTransactionStatus(ecsTlr.getIntermediateRequestDcbTransactionId(), newStatus, + ecsTlr.getIntermediateRequestTenantId()); + + log.info("updateTransactionStatuses:: updating secondary transaction status to {}", newStatus::getValue); + updateTransactionStatus(ecsTlr.getSecondaryRequestDcbTransactionId(), newStatus, + ecsTlr.getSecondaryRequestTenantId()); + } + + private void updateTransactionStatus(UUID transactionId, + TransactionStatus.StatusEnum newStatus, String tenantId) { + + if (transactionId == null) { + log.info("updateTransactionStatus:: transaction ID is null, doing nothing"); + return; + } + if (tenantId == null) { + log.info("updateTransactionStatus:: tenant ID is null, doing nothing"); + return; + } + + try { + var currentStatus = dcbService.getTransactionStatus(transactionId, tenantId).getStatus(); + log.info("updateTransactionStatus:: current transaction status: {}", currentStatus); + if (newStatus.getValue().equals(currentStatus.getValue())) { + log.info("updateTransactionStatus:: transaction status did not change, doing nothing"); + return; + } + log.info("updateTransactionStatus: changing status of transaction {} in tenant {} from {} to {}", + transactionId, tenantId, currentStatus.getValue(), newStatus.getValue()); + dcbService.updateTransactionStatus(transactionId, newStatus, tenantId); + } catch (FeignException.NotFound e) { + log.error("updateTransactionStatus:: transaction {} not found: {}", transactionId, e.getMessage()); + } catch (Exception e) { + log.error("updateTransactionStatus:: failed to update transaction status: {}", e::getMessage); + log.debug("updateTransactionStatus:: ", e); + } + } + + private void propagateChangesFromPrimaryToSecondaryRequest(EcsTlrEntity ecsTlr, + KafkaEvent event) { + + String secondaryRequestId = ecsTlr.getSecondaryRequestId().toString(); + String secondaryRequestTenantId = ecsTlr.getSecondaryRequestTenantId(); + Request primaryRequest = event.getData().getNewVersion(); + Request secondaryRequest = requestService.getRequestFromStorage( + secondaryRequestId, secondaryRequestTenantId); + + boolean shouldUpdateSecondaryRequest = false; + if (valueIsNotEqual(primaryRequest, secondaryRequest, Request::getRequestExpirationDate)) { + Date requestExpirationDate = primaryRequest.getRequestExpirationDate(); + log.info("propagateChangesFromPrimaryToSecondaryRequest:: request expiration date changed: {}", + requestExpirationDate); + secondaryRequest.setRequestExpirationDate(requestExpirationDate); + shouldUpdateSecondaryRequest = true; + } + if (valueIsNotEqual(primaryRequest, secondaryRequest, Request::getFulfillmentPreference)) { + FulfillmentPreferenceEnum fulfillmentPreference = primaryRequest.getFulfillmentPreference(); + log.info("propagateChangesFromPrimaryToSecondaryRequest:: fulfillment preference changed: {}", + fulfillmentPreference); + secondaryRequest.setFulfillmentPreference(fulfillmentPreference); + shouldUpdateSecondaryRequest = true; + } + if (valueIsNotEqual(primaryRequest, secondaryRequest, Request::getPickupServicePointId)) { + String pickupServicePointId = primaryRequest.getPickupServicePointId(); + log.info("propagateChangesFromPrimaryToSecondaryRequest:: pickup service point ID changed: {}", + pickupServicePointId); + secondaryRequest.setPickupServicePointId(pickupServicePointId); + shouldUpdateSecondaryRequest = true; + clonePickupServicePoint(ecsTlr, pickupServicePointId); + } + + if (!shouldUpdateSecondaryRequest) { + log.info("propagateChangesFromPrimaryToSecondaryRequest:: no relevant changes detected"); + return; + } + + log.info("propagateChangesFromPrimaryToSecondaryRequest:: updating secondary request"); + requestService.updateRequestInStorage(secondaryRequest, secondaryRequestTenantId); + log.info("propagateChangesFromPrimaryToSecondaryRequest:: secondary request updated"); + } + + private void clonePickupServicePoint(EcsTlrEntity ecsTlr, String pickupServicePointId) { + if (pickupServicePointId == null) { + log.info("clonePickupServicePoint:: pickupServicePointId is null, doing nothing"); + return; + } + log.info("clonePickupServicePoint:: ensuring that service point {} exists in lending tenant", + pickupServicePointId); + ServicePoint pickupServicePoint = executionService.executeSystemUserScoped( + ecsTlr.getPrimaryRequestTenantId(), () -> servicePointService.find(pickupServicePointId)); + executionService.executeSystemUserScoped(ecsTlr.getSecondaryRequestTenantId(), + () -> servicePointCloningService.clone(pickupServicePoint)); + } + + private static boolean valueIsNotEqual(T o1, T o2, Function valueExtractor) { + return !Objects.equals(valueExtractor.apply(o1), valueExtractor.apply(o2)); + } +} diff --git a/src/main/java/org/folio/service/impl/RequestServiceImpl.java b/src/main/java/org/folio/service/impl/RequestServiceImpl.java index d43ec863..869bac9a 100644 --- a/src/main/java/org/folio/service/impl/RequestServiceImpl.java +++ b/src/main/java/org/folio/service/impl/RequestServiceImpl.java @@ -163,7 +163,6 @@ public RequestWrapper createIntermediateRequest(Request intermediateRequest, Request request = circulationClient.createRequest(intermediateRequest); log.info("createIntermediateRequest:: intermediate request {} created in tenant {}", request.getId(), intermediateRequestTenantId); - log.info("createIntermediateRequest:: intermediate request: {}", () -> request); updateCirculationItemOnRequestCreation(circItem, request); diff --git a/src/main/java/org/folio/service/impl/TenantServiceImpl.java b/src/main/java/org/folio/service/impl/TenantServiceImpl.java index 61639fac..e1e67ca3 100644 --- a/src/main/java/org/folio/service/impl/TenantServiceImpl.java +++ b/src/main/java/org/folio/service/impl/TenantServiceImpl.java @@ -55,9 +55,6 @@ public String getPrimaryRequestTenantId(EcsTlrEntity ecsTlr) { log.info("getPrimaryRequestTenantId:: returning primaryRequestTenantId"); return ecsTlr.getPrimaryRequestTenantId(); - -// log.info("getPrimaryRequestTenantId:: getting borrowing tenant"); -// return HttpUtils.getTenantFromToken(); } @Override diff --git a/src/main/resources/swagger.api/ecs-tlr.yaml b/src/main/resources/swagger.api/ecs-tlr.yaml index dd60c16e..b1d4460c 100644 --- a/src/main/resources/swagger.api/ecs-tlr.yaml +++ b/src/main/resources/swagger.api/ecs-tlr.yaml @@ -103,6 +103,8 @@ components: $ref: 'schemas/request.json' requests: $ref: 'schemas/requests.json' + loan: + $ref: 'schemas/loan.json' searchInstancesResponse: $ref: 'schemas/search/searchInstancesResponse.yaml' searchItemResponse: diff --git a/src/main/resources/swagger.api/schemas/loan.json b/src/main/resources/swagger.api/schemas/loan.json new file mode 100644 index 00000000..1ab9653f --- /dev/null +++ b/src/main/resources/swagger.api/schemas/loan.json @@ -0,0 +1,166 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "title": "Loan", + "description": "Links the item with the patron and applies certain conditions based on policies", + "properties": { + "id": { + "description": "Unique ID (generated UUID) of the loan", + "type": "string" + }, + "userId": { + "description": "ID of the patron the item was lent to. Required for open loans, not required for closed loans (for anonymization).", + "type": "string" + }, + "proxyUserId": { + "description": "ID of the user representing a proxy for the patron", + "type": "string", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "itemId": { + "description": "ID of the item lent to the patron", + "type": "string" + }, + "itemEffectiveLocationIdAtCheckOut": { + "description": "The effective location, at the time of checkout, of the item loaned to the patron.", + "type": "string", + "$ref": "uuid.json" + }, + "status": { + "description": "Overall status of the loan", + "type": "object", + "properties": { + "name": { + "description": "Name of the status (currently can be any value, values commonly used are Open and Closed)", + "type": "string" + } + } + }, + "loanDate": { + "description": "Date time when the loan began (typically represented according to rfc3339 section-5.6. Has not had the date-time format validation applied as was not supported at point of introduction and would now be a breaking change)", + "type": "string" + }, + "dueDate": { + "description": "Date time when the item is due to be returned", + "type": "string", + "format": "date-time" + }, + "returnDate": { + "description": "Date time when the item is returned and the loan ends (typically represented according to rfc3339 section-5.6. Has not had the date-time format validation applied as was not supported at point of introduction and would now be a breaking change)", + "type": "string" + }, + "systemReturnDate" : { + "description": "Date time when the returned item is actually processed", + "type": "string", + "format": "date-time" + }, + "action": { + "description": "Last action performed on a loan (currently can be any value, values commonly used are checkedout and checkedin)", + "type": "string" + }, + "actionComment": { + "description": "Comment to last action performed on a loan", + "type": "string" + }, + "itemStatus": { + "description": "Last item status used in relation to this loan (currently can be any value, values commonly used are Checked out and Available)", + "type": "string" + }, + "renewalCount": { + "description": "Count of how many times a loan has been renewed (incremented by the client)", + "type": "integer" + }, + "loanPolicyId": { + "description": "ID of last policy used in relation to this loan", + "type": "string" + }, + "checkoutServicePointId": { + "description": "ID of the Service Point where the last checkout occured", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", + "type": "string" + }, + "checkinServicePointId": { + "description": "ID of the Service Point where the last checkin occured", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", + "type": "string" + }, + "patronGroupIdAtCheckout": { + "description": "Patron Group Id at checkout", + "type": "string" + }, + "dueDateChangedByRecall": { + "description": "Indicates whether or not this loan had its due date modified by a recall on the loaned item", + "type": "boolean" + }, + "isDcb": { + "description": "Indicates whether or not this loan is associated for DCB use case", + "type": "boolean" + }, + "declaredLostDate" : { + "description": "Date and time the item was declared lost during this loan", + "type": "string", + "format": "date-time" + }, + "claimedReturnedDate": { + "description": "Date and time the item was claimed returned for this loan", + "type": "string", + "format": "date-time" + }, + "overdueFinePolicyId": { + "description": "ID of overdue fines policy at the time the item is check-in or renewed", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", + "type": "string" + }, + "lostItemPolicyId": { + "description": "ID of lost item policy which determines when the item ages to lost and the associated fees or the associated fees if the patron declares the item lost.", + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$", + "type": "string" + }, + "metadata": { + "description": "Metadata about creation and changes to loan, provided by the server (client should not provide)", + "type": "object", + "$ref": "metadata.json" + }, + "agedToLostDelayedBilling": { + "description": "Aged to Lost Delayed Billing processing", + "type": "object", + "properties": { + "lostItemHasBeenBilled": { + "description": "Indicates if the aged to lost fee has been billed (for use where delayed billing is set up)", + "type": "boolean" + }, + "dateLostItemShouldBeBilled": { + "description": "Indicates when the aged to lost fee should be billed (for use where delayed billing is set up)", + "type": "string", + "format": "date-time" + }, + "agedToLostDate": { + "description": "Date and time the item was aged to lost for this loan", + "type": "string", + "format": "date-time" + } + } + }, + "reminders" : { + "description": "Information about reminders for overdue loan", + "type": "object", + "properties": { + "lastFeeBilled": { + "description": "Information about the most recent reminder fee billing", + "type": "object", + "properties": { + "number": { + "description": "Last reminder fee billed, sequence number", + "type": "integer" + }, + "date": { + "description": "Last reminder fee billed, date", + "type": "string", + "format": "date-time" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/test/java/org/folio/listener/KafkaEventListenerTest.java b/src/test/java/org/folio/listener/KafkaEventListenerTest.java index 759f8272..fe5f8f5d 100644 --- a/src/test/java/org/folio/listener/KafkaEventListenerTest.java +++ b/src/test/java/org/folio/listener/KafkaEventListenerTest.java @@ -8,6 +8,7 @@ import java.util.Map; import org.folio.listener.kafka.KafkaEventListener; +import org.folio.service.impl.LoanEventHandler; import org.folio.service.impl.RequestBatchUpdateEventHandler; import org.folio.service.impl.RequestEventHandler; import org.folio.service.impl.UserEventHandler; @@ -24,6 +25,8 @@ class KafkaEventListenerTest { @Mock RequestEventHandler requestEventHandler; @Mock + LoanEventHandler loanEventHandler; + @Mock RequestBatchUpdateEventHandler requestBatchEventHandler; @Mock SystemUserScopedExecutionService systemUserScopedExecutionService; @@ -37,8 +40,8 @@ void shouldHandleExceptionInEventHandler() { doThrow(new NullPointerException("NPE")).when(systemUserScopedExecutionService) .executeAsyncSystemUserScoped(any(), any()); KafkaEventListener kafkaEventListener = new KafkaEventListener(requestEventHandler, - requestBatchEventHandler, systemUserScopedExecutionService, userGroupEventHandler, - userEventHandler); + loanEventHandler, requestBatchEventHandler, systemUserScopedExecutionService, + userGroupEventHandler, userEventHandler); kafkaEventListener.handleRequestEvent("{}", new MessageHeaders(Map.of(TENANT, "default".getBytes())));