diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json
index 6955a729ab..c460669204 100644
--- a/descriptors/ModuleDescriptor-template.json
+++ b/descriptors/ModuleDescriptor-template.json
@@ -636,6 +636,24 @@
"unit": "minute",
"delay": "3"
},
+ {
+ "methods": [
+ "POST"
+ ],
+ "pathPattern": "/circulation/actual-cost-expiration-by-timeout",
+ "modulePermissions": [
+ "circulation-storage.loans.item.put",
+ "inventory-storage.items.item.put",
+ "actual-cost-record-storage.actual-cost-records.collection.get",
+ "accounts.collection.get",
+ "circulation.internal.fetch-items",
+ "lost-item-fees-policies.collection.get",
+ "circulation-storage.loans.collection.get",
+ "pubsub.publish.post"
+ ],
+ "unit": "minute",
+ "delay": "20"
+ },
{
"methods": [
"POST"
diff --git a/pom.xml b/pom.xml
index 79db53b5c9..bd56d6154e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,7 +30,7 @@
2.17.1
UTF-8
UTF-8
- 1.18.16
+ 1.18.22
5.2.22.RELEASE
3.9.1
diff --git a/src/main/java/org/folio/circulation/CirculationVerticle.java b/src/main/java/org/folio/circulation/CirculationVerticle.java
index 3c9e38403a..4bc1d7821a 100644
--- a/src/main/java/org/folio/circulation/CirculationVerticle.java
+++ b/src/main/java/org/folio/circulation/CirculationVerticle.java
@@ -13,6 +13,7 @@
import org.folio.circulation.resources.DeclareLostResource;
import org.folio.circulation.resources.DueDateNotRealTimeScheduledNoticeProcessingResource;
import org.folio.circulation.resources.EndPatronActionSessionResource;
+import org.folio.circulation.resources.ExpiredActualCostProcessingResource;
import org.folio.circulation.resources.ExpiredSessionProcessingResource;
import org.folio.circulation.resources.FeeFineScheduledNoticeProcessingResource;
import org.folio.circulation.resources.ItemsInTransitResource;
@@ -138,6 +139,7 @@ public void start(Promise startFuture) {
startFuture.fail(result.cause());
}
});
+ new ExpiredActualCostProcessingResource(client).register(router);
}
@Override
diff --git a/src/main/java/org/folio/circulation/domain/ActualCostRecord.java b/src/main/java/org/folio/circulation/domain/ActualCostRecord.java
index 5a42ec1da5..0d975f8272 100644
--- a/src/main/java/org/folio/circulation/domain/ActualCostRecord.java
+++ b/src/main/java/org/folio/circulation/domain/ActualCostRecord.java
@@ -19,7 +19,7 @@ public class ActualCostRecord {
private String userBarcode;
private String loanId;
private ItemLossType itemLossType;
- private String dateOfLoss;
+ private ZonedDateTime dateOfLoss;
private String title;
private Collection identifiers;
private String itemBarcode;
@@ -31,4 +31,5 @@ public class ActualCostRecord {
private String feeFineTypeId;
private String feeFineType;
private ZonedDateTime creationDate;
+ private ZonedDateTime expirationDate;
}
diff --git a/src/main/java/org/folio/circulation/domain/ItemRelatedRecord.java b/src/main/java/org/folio/circulation/domain/ItemRelatedRecord.java
index 45b88be89d..9a3cf88053 100644
--- a/src/main/java/org/folio/circulation/domain/ItemRelatedRecord.java
+++ b/src/main/java/org/folio/circulation/domain/ItemRelatedRecord.java
@@ -2,4 +2,5 @@
public interface ItemRelatedRecord {
String getItemId();
+ ItemRelatedRecord withItem(Item item);
}
diff --git a/src/main/java/org/folio/circulation/domain/Loan.java b/src/main/java/org/folio/circulation/domain/Loan.java
index e9fd96f457..4c5786449f 100644
--- a/src/main/java/org/folio/circulation/domain/Loan.java
+++ b/src/main/java/org/folio/circulation/domain/Loan.java
@@ -88,6 +88,7 @@ public class Loan implements ItemRelatedRecord, UserRelatedRecord {
private final Policies policies;
private final Collection accounts;
+ private final ActualCostRecord actualCostRecord;
public static Loan from(JsonObject representation) {
defaultStatusAndAction(representation);
@@ -100,7 +101,7 @@ public static Loan from(JsonObject representation) {
return new Loan(representation, null, null, null, null, null,
getDateTimeProperty(representation, DUE_DATE), getDateTimeProperty(representation, DUE_DATE),
- new Policies(loanPolicy, overdueFinePolicy, lostItemPolicy), emptyList());
+ new Policies(loanPolicy, overdueFinePolicy, lostItemPolicy), emptyList(), null);
}
public JsonObject asJson() {
@@ -272,7 +273,7 @@ public Item getItem() {
public Loan replaceRepresentation(JsonObject newRepresentation) {
return new Loan(newRepresentation, item, user, proxy, checkinServicePoint,
- checkoutServicePoint, originalDueDate, previousDueDate, policies, accounts);
+ checkoutServicePoint, originalDueDate, previousDueDate, policies, accounts, actualCostRecord);
}
public Loan withItem(Item newItem) {
@@ -283,7 +284,7 @@ public Loan withItem(Item newItem) {
}
return new Loan(newRepresentation, newItem, user, proxy, checkinServicePoint,
- checkoutServicePoint, originalDueDate, previousDueDate, policies, accounts);
+ checkoutServicePoint, originalDueDate, previousDueDate, policies, accounts, actualCostRecord);
}
public User getUser() {
@@ -298,7 +299,16 @@ public Loan withUser(User newUser) {
}
return new Loan(newRepresentation, item, newUser, proxy, checkinServicePoint,
- checkoutServicePoint, originalDueDate, previousDueDate, policies, accounts);
+ checkoutServicePoint, originalDueDate, previousDueDate, policies, accounts, actualCostRecord);
+ }
+
+ public Loan withActualCostRecord(ActualCostRecord actualCostRecord) {
+ return new Loan(representation, item, user, proxy, checkinServicePoint, checkoutServicePoint,
+ originalDueDate, previousDueDate, policies, accounts, actualCostRecord);
+ }
+
+ public ActualCostRecord getActualCostRecord() {
+ return actualCostRecord;
}
public Loan withPatronGroupAtCheckout(PatronGroup patronGroup) {
@@ -325,22 +335,22 @@ Loan withProxy(User newProxy) {
}
return new Loan(newRepresentation, item, user, newProxy, checkinServicePoint,
- checkoutServicePoint, originalDueDate, previousDueDate, policies, accounts);
+ checkoutServicePoint, originalDueDate, previousDueDate, policies, accounts, actualCostRecord);
}
public Loan withCheckinServicePoint(ServicePoint newCheckinServicePoint) {
return new Loan(representation, item, user, proxy, newCheckinServicePoint,
- checkoutServicePoint, originalDueDate, previousDueDate, policies, accounts);
+ checkoutServicePoint, originalDueDate, previousDueDate, policies, accounts, actualCostRecord);
}
public Loan withCheckoutServicePoint(ServicePoint newCheckoutServicePoint) {
return new Loan(representation, item, user, proxy, checkinServicePoint,
- newCheckoutServicePoint, originalDueDate, previousDueDate, policies, accounts);
+ newCheckoutServicePoint, originalDueDate, previousDueDate, policies, accounts, actualCostRecord);
}
public Loan withAccounts(Collection newAccounts) {
return new Loan(representation, item, user, proxy, checkinServicePoint,
- checkoutServicePoint, originalDueDate, previousDueDate, policies, newAccounts);
+ checkoutServicePoint, originalDueDate, previousDueDate, policies, newAccounts, actualCostRecord);
}
public Loan withLoanPolicy(LoanPolicy newLoanPolicy) {
@@ -348,7 +358,7 @@ public Loan withLoanPolicy(LoanPolicy newLoanPolicy) {
return new Loan(representation, item, user, proxy, checkinServicePoint,
checkoutServicePoint, originalDueDate, previousDueDate,
- policies.withLoanPolicy(newLoanPolicy), accounts);
+ policies.withLoanPolicy(newLoanPolicy), accounts, actualCostRecord);
}
public Loan withOverdueFinePolicy(OverdueFinePolicy newOverdueFinePolicy) {
@@ -356,7 +366,7 @@ public Loan withOverdueFinePolicy(OverdueFinePolicy newOverdueFinePolicy) {
return new Loan(representation, item, user, proxy, checkinServicePoint,
checkoutServicePoint, originalDueDate, previousDueDate,
- policies.withOverdueFinePolicy(newOverdueFinePolicy), accounts);
+ policies.withOverdueFinePolicy(newOverdueFinePolicy), accounts, actualCostRecord);
}
public Loan withLostItemPolicy(LostItemPolicy newLostItemPolicy) {
@@ -364,7 +374,7 @@ public Loan withLostItemPolicy(LostItemPolicy newLostItemPolicy) {
return new Loan(representation, item, user, proxy, checkinServicePoint,
checkoutServicePoint, originalDueDate, previousDueDate,
- policies.withLostItemPolicy(newLostItemPolicy), accounts);
+ policies.withLostItemPolicy(newLostItemPolicy), accounts, actualCostRecord);
}
public String getLoanPolicyId() {
@@ -625,7 +635,7 @@ public void closeLoanAsLostAndPaid() {
public Loan copy() {
final JsonObject representationCopy = representation.copy();
return new Loan(representationCopy, item, user, proxy, checkinServicePoint,
- checkoutServicePoint, originalDueDate, previousDueDate, policies, accounts);
+ checkoutServicePoint, originalDueDate, previousDueDate, policies, accounts, actualCostRecord);
}
public Loan ageOverdueItemToLost(ZonedDateTime ageToLostDate) {
diff --git a/src/main/java/org/folio/circulation/domain/policy/lostitem/LostItemPolicy.java b/src/main/java/org/folio/circulation/domain/policy/lostitem/LostItemPolicy.java
index 9c721f8a11..d5a56363ce 100644
--- a/src/main/java/org/folio/circulation/domain/policy/lostitem/LostItemPolicy.java
+++ b/src/main/java/org/folio/circulation/domain/policy/lostitem/LostItemPolicy.java
@@ -34,6 +34,7 @@ public class LostItemPolicy extends Policy {
private final Period patronBilledAfterItemAgedToLostInterval;
private final Period recalledItemAgedToLostAfterOverdueInterval;
private final Period patronBilledAfterRecalledItemAgedToLostInterval;
+ private final Period lostItemChargeFeeFineInterval;
// There is no separate age to lost processing fee but there is a flag
// that turns on/off the fee, but we're modelling it as a separate fee
// to simplify logic.
@@ -45,7 +46,7 @@ private LostItemPolicy(String id, String name, AutomaticallyChargeableFee declar
Period itemAgedToLostAfterOverdueInterval, Period patronBilledAfterItemAgedToLostInterval,
Period recalledItemAgedToLostAfterOverdueInterval,
Period patronBilledAfterRecalledItemAgedToLostInterval,
- AutomaticallyChargeableFee ageToLostProcessingFee) {
+ AutomaticallyChargeableFee ageToLostProcessingFee, Period lostItemChargeFeeFineInterval) {
super(id, name);
this.declareLostProcessingFee = declareLostProcessingFee;
@@ -60,6 +61,7 @@ private LostItemPolicy(String id, String name, AutomaticallyChargeableFee declar
this.patronBilledAfterRecalledItemAgedToLostInterval =
patronBilledAfterRecalledItemAgedToLostInterval;
this.ageToLostProcessingFee = ageToLostProcessingFee;
+ this.lostItemChargeFeeFineInterval = lostItemChargeFeeFineInterval;
}
public static LostItemPolicy from(JsonObject lostItemPolicy) {
@@ -76,7 +78,8 @@ public static LostItemPolicy from(JsonObject lostItemPolicy) {
getPeriodPropertyOrEmpty(lostItemPolicy, "patronBilledAfterAgedLost"),
getPeriodPropertyOrEmpty(lostItemPolicy, "recalledItemAgedLostOverdue"),
getPeriodPropertyOrEmpty(lostItemPolicy, "patronBilledAfterRecalledItemAgedLost"),
- getProcessingFee(lostItemPolicy, "chargeAmountItemSystem")
+ getProcessingFee(lostItemPolicy, "chargeAmountItemSystem"),
+ getPeriodPropertyOrEmpty(lostItemPolicy, "lostItemChargeFeeFine")
);
}
@@ -162,6 +165,10 @@ public boolean canAgeLoanToLost(boolean isRecalled, ZonedDateTime loanDueDate) {
return periodShouldPassSinceOverdue.hasPassedSinceDateTillNow(loanDueDate);
}
+ public ZonedDateTime calculateFeeFineChargingPeriodExpirationDateTime(ZonedDateTime lostTime) {
+ return lostItemChargeFeeFineInterval.plusDate(lostTime);
+ }
+
public ZonedDateTime calculateDateTimeWhenPatronBilledForAgedToLost(
boolean isRecalled, ZonedDateTime ageToLostDate) {
@@ -189,7 +196,7 @@ private static class UnknownLostItemPolicy extends LostItemPolicy {
super(id, null, noAutomaticallyChargeableFee(), noAutomaticallyChargeableFee(),
noActualCostFee(), zeroDurationPeriod(), false, false,
zeroDurationPeriod(), zeroDurationPeriod(), zeroDurationPeriod(),
- zeroDurationPeriod(), noAutomaticallyChargeableFee());
+ zeroDurationPeriod(), noAutomaticallyChargeableFee(), zeroDurationPeriod());
}
}
}
diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/ActualCostRecordRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/ActualCostRecordRepository.java
index 9eeb16b556..c3223aca34 100644
--- a/src/main/java/org/folio/circulation/infrastructure/storage/ActualCostRecordRepository.java
+++ b/src/main/java/org/folio/circulation/infrastructure/storage/ActualCostRecordRepository.java
@@ -1,25 +1,45 @@
package org.folio.circulation.infrastructure.storage;
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static java.util.function.Function.identity;
+import static org.folio.circulation.support.fetching.MultipleCqlIndexValuesCriteria.byIndex;
+import static org.folio.circulation.support.fetching.RecordFetching.findWithMultipleCqlIndexValues;
import static org.folio.circulation.support.http.ResponseMapping.forwardOnFailure;
import static org.folio.circulation.support.http.ResponseMapping.mapUsingJson;
+import static org.folio.circulation.support.http.client.CqlQuery.exactMatch;
+import static org.folio.circulation.support.http.client.PageLimit.one;
import static org.folio.circulation.support.results.Result.ofAsync;
+import static org.folio.circulation.support.results.Result.succeeded;
+import static org.folio.circulation.support.results.ResultBinding.mapResult;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
import org.folio.circulation.domain.ActualCostRecord;
+import org.folio.circulation.domain.Loan;
import org.folio.circulation.domain.MultipleRecords;
import org.folio.circulation.storage.mappers.ActualCostRecordMapper;
import org.folio.circulation.support.Clients;
import org.folio.circulation.support.CollectionResourceClient;
+import org.folio.circulation.support.fetching.CqlQueryFinder;
import org.folio.circulation.support.http.client.CqlQuery;
import org.folio.circulation.support.http.client.PageLimit;
import org.folio.circulation.support.http.client.Response;
import org.folio.circulation.support.http.client.ResponseInterpreter;
import org.folio.circulation.support.results.Result;
+import io.vertx.core.json.JsonObject;
+
public class ActualCostRecordRepository {
private final CollectionResourceClient actualCostRecordStorageClient;
+ private static final String ACTUAL_COST_RECORDS = "actualCostRecords";
+ private static final String LOAN_ID_FIELD_NAME = "loanId";
+
public ActualCostRecordRepository(Clients clients) {
actualCostRecordStorageClient = clients.actualCostRecordsStorage();
}
@@ -52,6 +72,47 @@ public CompletableFuture> getActualCostRecordByAccountI
}
private Result> mapResponseToActualCostRecords(Response response) {
- return MultipleRecords.from(response, ActualCostRecordMapper::toDomain, "actualCostRecords");
+ return MultipleRecords.from(response, ActualCostRecordMapper::toDomain, ACTUAL_COST_RECORDS);
+ }
+
+ public CompletableFuture> findByLoan(Result loanResult) {
+ return loanResult.after(loan -> createActualCostRecordCqlFinder().findByQuery(
+ exactMatch(LOAN_ID_FIELD_NAME, loan.getId()), one())
+ .thenApply(records -> records.map(MultipleRecords::firstOrNull))
+ .thenApply(mapResult(ActualCostRecordMapper::toDomain))
+ .thenApply(mapResult(loan::withActualCostRecord)));
+ }
+
+ public CompletableFuture>> fetchActualCostRecords(
+ MultipleRecords multipleLoans) {
+
+ if (multipleLoans.getRecords().isEmpty()) {
+ return completedFuture(succeeded(multipleLoans));
+ }
+
+ return buildLoanIdToActualCostRecordMap(multipleLoans.getRecords())
+ .thenApply(r -> r.map(actualCostRecordMap -> multipleLoans.mapRecords(
+ loan -> loan.withActualCostRecord(actualCostRecordMap.getOrDefault(loan.getId(), null)))));
+ }
+
+ private CompletableFuture>> buildLoanIdToActualCostRecordMap(
+ Collection loans) {
+
+ final Set loanIds = loans.stream()
+ .filter(Objects::nonNull)
+ .map(Loan::getId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+
+ return findWithMultipleCqlIndexValues(actualCostRecordStorageClient,
+ ACTUAL_COST_RECORDS, ActualCostRecordMapper::toDomain)
+ .find(byIndex(LOAN_ID_FIELD_NAME, loanIds))
+ .thenApply(mapResult(r -> r.toMap(ActualCostRecord::getLoanId)));
+ }
+
+ private CqlQueryFinder createActualCostRecordCqlFinder() {
+ return new CqlQueryFinder<>(actualCostRecordStorageClient, ACTUAL_COST_RECORDS,
+ identity());
}
+
}
diff --git a/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java b/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java
index 609ba1d0a3..f46b043b73 100644
--- a/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java
+++ b/src/main/java/org/folio/circulation/infrastructure/storage/inventory/ItemRepository.java
@@ -256,9 +256,16 @@ private CompletableFuture> fetchItemByBarcode(String barcode) {
}
public CompletableFuture>>
- fetchItemsWithHoldings(Result> result, BiFunction includeItemMap) {
+ fetchItemsWithHoldings(Result> result, BiFunction withItemMapper) {
- return fetchItemsFor(result, includeItemMap, this::fetchItemsWithHoldingsRecords);
+ return fetchItemsFor(result, withItemMapper, this::fetchItems);
+ }
+
+ public CompletableFuture>>
+ fetchItems(Result> result) {
+
+ return fetchItemsFor(result, (itemRelatedRecord, item) -> (T) itemRelatedRecord.withItem(item),
+ this::fetchItems);
}
public CompletableFuture>>
@@ -309,13 +316,6 @@ private CompletableFuture>> fetchFor(
.thenComposeAsync(this::fetchItemsRelatedRecords);
}
- private CompletableFuture>> fetchItemsWithHoldingsRecords(
- Collection itemIds) {
-
- return fetchItems(itemIds)
- .thenComposeAsync(this::fetchHoldingsRecords);
- }
-
public CompletableFuture> fetchItemRelatedRecords(Result- itemResult) {
return itemResult.combineAfter(this::fetchHoldingsRecord, Item::withHoldings)
.thenComposeAsync(combineAfter(this::fetchInstance, Item::withInstance))
diff --git a/src/main/java/org/folio/circulation/resources/ExpiredActualCostProcessingResource.java b/src/main/java/org/folio/circulation/resources/ExpiredActualCostProcessingResource.java
new file mode 100644
index 0000000000..57b1b61224
--- /dev/null
+++ b/src/main/java/org/folio/circulation/resources/ExpiredActualCostProcessingResource.java
@@ -0,0 +1,58 @@
+package org.folio.circulation.resources;
+
+import org.folio.circulation.infrastructure.storage.ActualCostRecordRepository;
+import org.folio.circulation.infrastructure.storage.feesandfines.AccountRepository;
+import org.folio.circulation.infrastructure.storage.inventory.ItemRepository;
+import org.folio.circulation.infrastructure.storage.loans.LoanRepository;
+import org.folio.circulation.infrastructure.storage.loans.LostItemPolicyRepository;
+import org.folio.circulation.infrastructure.storage.users.UserRepository;
+import org.folio.circulation.services.CloseLoanWithLostItemService;
+import org.folio.circulation.services.EventPublisher;
+import org.folio.circulation.services.actualcostrecord.ActualCostRecordExpirationService;
+import org.folio.circulation.support.RouteRegistration;
+import org.folio.circulation.support.fetching.PageableFetcher;
+import org.folio.circulation.support.http.server.NoContentResponse;
+import org.folio.circulation.support.http.server.WebContext;
+
+import io.vertx.core.http.HttpClient;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.RoutingContext;
+import static org.folio.circulation.support.Clients.create;
+import static org.folio.circulation.support.results.MappingFunctions.toFixedValue;
+
+public class ExpiredActualCostProcessingResource extends Resource {
+ public ExpiredActualCostProcessingResource(HttpClient client) {
+ super(client);
+ }
+
+ @Override
+ public void register(Router router) {
+ new RouteRegistration("/circulation/actual-cost-expiration-by-timeout", router)
+ .create(this::process);
+ }
+
+ private void process(RoutingContext routingContext) {
+ var context = new WebContext(routingContext);
+ var clients = create(context, client);
+
+ var eventPublisher = new EventPublisher(routingContext);
+ var itemRepository = new ItemRepository(clients);
+ var userRepository = new UserRepository(clients);
+ var loanRepository = new LoanRepository(clients, itemRepository, userRepository);
+ var accountRepository = new AccountRepository(clients);
+ var lostItemPolicyRepository = new LostItemPolicyRepository(clients);
+ var actualCostRecordRepository = new ActualCostRecordRepository(clients);
+ var closeLoanWithLostItemService = new CloseLoanWithLostItemService(loanRepository,
+ itemRepository, accountRepository, lostItemPolicyRepository,
+ eventPublisher, actualCostRecordRepository);
+ var loanPageableFetcher = new PageableFetcher<>(loanRepository);
+ var actualCostRecordExpirationService = new ActualCostRecordExpirationService(
+ loanPageableFetcher, closeLoanWithLostItemService, itemRepository);
+
+ actualCostRecordExpirationService.expireActualCostRecords()
+ .thenApply(r -> r.map(toFixedValue(NoContentResponse::noContent)))
+ .thenAccept(context::writeResultToHttpResponse);
+ }
+
+
+}
diff --git a/src/main/java/org/folio/circulation/resources/handlers/LoanRelatedFeeFineClosedHandlerResource.java b/src/main/java/org/folio/circulation/resources/handlers/LoanRelatedFeeFineClosedHandlerResource.java
index 2742ae6495..c82109fa49 100644
--- a/src/main/java/org/folio/circulation/resources/handlers/LoanRelatedFeeFineClosedHandlerResource.java
+++ b/src/main/java/org/folio/circulation/resources/handlers/LoanRelatedFeeFineClosedHandlerResource.java
@@ -1,8 +1,6 @@
package org.folio.circulation.resources.handlers;
-import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.folio.circulation.domain.EventType.LOAN_RELATED_FEE_FINE_CLOSED;
-import static org.folio.circulation.domain.FeeFine.lostItemFeeTypes;
import static org.folio.circulation.domain.subscribers.LoanRelatedFeeFineClosedEvent.fromJson;
import static org.folio.circulation.support.Clients.create;
import static org.folio.circulation.support.ValidationErrorFailure.singleValidationError;
@@ -13,26 +11,24 @@
import java.util.concurrent.CompletableFuture;
-import org.folio.circulation.StoreLoanAndItem;
-import org.folio.circulation.domain.Account;
-import org.folio.circulation.domain.Loan;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.folio.circulation.domain.subscribers.LoanRelatedFeeFineClosedEvent;
+import org.folio.circulation.infrastructure.storage.ActualCostRecordRepository;
import org.folio.circulation.infrastructure.storage.feesandfines.AccountRepository;
import org.folio.circulation.infrastructure.storage.inventory.ItemRepository;
import org.folio.circulation.infrastructure.storage.loans.LoanRepository;
import org.folio.circulation.infrastructure.storage.loans.LostItemPolicyRepository;
import org.folio.circulation.infrastructure.storage.users.UserRepository;
import org.folio.circulation.resources.Resource;
+import org.folio.circulation.services.CloseLoanWithLostItemService;
import org.folio.circulation.services.EventPublisher;
-import org.folio.circulation.support.Clients;
import org.folio.circulation.support.RouteRegistration;
import org.folio.circulation.support.http.server.NoContentResponse;
import org.folio.circulation.support.http.server.ValidationError;
import org.folio.circulation.support.http.server.WebContext;
import org.folio.circulation.support.results.CommonFailures;
import org.folio.circulation.support.results.Result;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
import io.vertx.core.http.HttpClient;
import io.vertx.ext.web.Router;
@@ -54,13 +50,19 @@ public void register(Router router) {
private void handleFeeFineClosedEvent(RoutingContext routingContext) {
final WebContext context = new WebContext(routingContext);
- final EventPublisher eventPublisher = new EventPublisher(routingContext);
+ final var eventPublisher = new EventPublisher(routingContext);
+ final var clients = create(context, client);
+ final var itemRepository = new ItemRepository(clients);
+ final var userRepository = new UserRepository(clients);
+ final var loanRepository = new LoanRepository(clients, itemRepository, userRepository);
+ final var closeLoanWithLostItemService = new CloseLoanWithLostItemService(loanRepository,
+ itemRepository, new AccountRepository(clients), new LostItemPolicyRepository(clients),
+ eventPublisher, new ActualCostRecordRepository(clients));
log.info("Event {} received: {}", LOAN_RELATED_FEE_FINE_CLOSED, routingContext.getBodyAsString());
createAndValidateRequest(routingContext)
- .after(request -> processEvent(context, request, eventPublisher))
- .thenCompose(r -> r.after(eventPublisher::publishClosedLoanEvent))
+ .after(request -> processEvent(loanRepository, request, closeLoanWithLostItemService))
.exceptionally(CommonFailures::failedDueToServerError)
.thenApply(r -> r.map(toFixedValue(NoContentResponse::noContent)))
.thenAccept(result -> result.applySideEffect(context::write, failure -> {
@@ -71,70 +73,11 @@ private void handleFeeFineClosedEvent(RoutingContext routingContext) {
}));
}
- private CompletableFuture> processEvent(WebContext context,
- LoanRelatedFeeFineClosedEvent event, EventPublisher eventPublisher) {
-
- final Clients clients = create(context, client);
- final var itemRepository = new ItemRepository(clients);
- final var userRepository = new UserRepository(clients);
- final var loanRepository = new LoanRepository(clients, itemRepository, userRepository);
- final var accountRepository = new AccountRepository(clients);
- final var lostItemPolicyRepository = new LostItemPolicyRepository(clients);
+ private CompletableFuture> processEvent(LoanRepository loanRepository,
+ LoanRelatedFeeFineClosedEvent event, CloseLoanWithLostItemService closeLoanWithLostItemService) {
return loanRepository.getById(event.getLoanId())
- .thenCompose(r -> r.after(loan -> {
- if (loan.isItemLost()) {
- return closeLoanWithLostItemIfLostFeesResolved(loan,
- loanRepository, itemRepository, accountRepository,
- lostItemPolicyRepository, eventPublisher);
- }
-
- return completedFuture(succeeded(loan));
- }));
- }
-
- private CompletableFuture> closeLoanWithLostItemIfLostFeesResolved(
- Loan loan, LoanRepository loanRepository,
- ItemRepository itemRepository, AccountRepository accountRepository,
- LostItemPolicyRepository lostItemPolicyRepository, EventPublisher eventPublisher) {
-
- return accountRepository.findAccountsForLoan(loan)
- .thenComposeAsync(lostItemPolicyRepository::findLostItemPolicyForLoan)
- .thenCompose(r -> r.after(l -> closeLoanAndUpdateItem(l, loanRepository,
- itemRepository, eventPublisher)));
- }
-
- public CompletableFuture> closeLoanAndUpdateItem(Loan loan,
- LoanRepository loanRepository, ItemRepository itemRepository, EventPublisher eventPublisher) {
-
- if (!allLostFeesClosed(loan)) {
- return completedFuture(succeeded(loan));
- }
-
- boolean wasLoanOpen = loan.isOpen();
- loan.closeLoanAsLostAndPaid();
-
- return new StoreLoanAndItem(loanRepository, itemRepository).updateLoanAndItemInStorage(loan)
- .thenCompose(r -> r.after(l -> publishLoanClosedEvent(l, wasLoanOpen, eventPublisher)));
- }
-
- private CompletableFuture> publishLoanClosedEvent(Loan loan, boolean wasLoanOpen,
- EventPublisher eventPublisher) {
-
- return wasLoanOpen && loan.isClosed()
- ? eventPublisher.publishLoanClosedEvent(loan)
- : completedFuture(succeeded(loan));
- }
-
- private boolean allLostFeesClosed(Loan loan) {
- if (loan.getLostItemPolicy().hasActualCostFee()) {
- // Actual cost fee is processed manually
- return false;
- }
-
- return loan.getAccounts().stream()
- .filter(account -> lostItemFeeTypes().contains(account.getFeeFineType()))
- .allMatch(Account::isClosed);
+ .thenCompose(r -> r.after(closeLoanWithLostItemService::closeLoanWithLostItemFeesPaid));
}
private Result createAndValidateRequest(RoutingContext context) {
diff --git a/src/main/java/org/folio/circulation/services/CloseLoanWithLostItemService.java b/src/main/java/org/folio/circulation/services/CloseLoanWithLostItemService.java
new file mode 100644
index 0000000000..31c799ec53
--- /dev/null
+++ b/src/main/java/org/folio/circulation/services/CloseLoanWithLostItemService.java
@@ -0,0 +1,117 @@
+package org.folio.circulation.services;
+
+import static java.util.concurrent.CompletableFuture.completedFuture;
+import static org.folio.circulation.domain.FeeFine.LOST_ITEM_ACTUAL_COST_FEE_TYPE;
+import static org.folio.circulation.domain.FeeFine.lostItemFeeTypes;
+import static org.folio.circulation.support.results.Result.succeeded;
+import static org.folio.circulation.support.utils.ClockUtil.getZonedDateTime;
+
+import java.time.ZonedDateTime;
+import java.util.concurrent.CompletableFuture;
+
+import org.folio.circulation.StoreLoanAndItem;
+import org.folio.circulation.domain.Account;
+import org.folio.circulation.domain.ActualCostRecord;
+import org.folio.circulation.domain.Loan;
+import org.folio.circulation.infrastructure.storage.ActualCostRecordRepository;
+import org.folio.circulation.infrastructure.storage.feesandfines.AccountRepository;
+import org.folio.circulation.infrastructure.storage.inventory.ItemRepository;
+import org.folio.circulation.infrastructure.storage.loans.LoanRepository;
+import org.folio.circulation.infrastructure.storage.loans.LostItemPolicyRepository;
+import org.folio.circulation.support.results.Result;
+public class CloseLoanWithLostItemService {
+
+ private final LoanRepository loanRepository;
+
+ private final LostItemPolicyRepository lostItemPolicyRepository;
+
+ private final ItemRepository itemRepository;
+
+ private final AccountRepository accountRepository;
+
+ private final EventPublisher eventPublisher;
+
+ private final ActualCostRecordRepository actualCostRecordRepository;
+
+ public CloseLoanWithLostItemService(LoanRepository loanRepository, ItemRepository itemRepository,
+ AccountRepository accountRepository, LostItemPolicyRepository lostItemPolicyRepository,
+ EventPublisher eventPublisher, ActualCostRecordRepository actualCostRecordRepository) {
+
+ this.loanRepository = loanRepository;
+ this.itemRepository = itemRepository;
+ this.accountRepository = accountRepository;
+ this.lostItemPolicyRepository = lostItemPolicyRepository;
+ this.eventPublisher = eventPublisher;
+ this.actualCostRecordRepository = actualCostRecordRepository;
+ }
+
+ public CompletableFuture> closeLoanWithLostItemFeesPaid(Loan loan) {
+ if (loan == null || !loan.isItemLost()) {
+ return completedFuture(Result.succeeded(null));
+ }
+
+ return fetchLoanFeeFineData(loan)
+ .thenCompose(r -> r.after(this::closeLoanWithLostItemFeesPaidAndPublishEvents));
+ }
+
+ private CompletableFuture> closeLoanWithLostItemFeesPaidAndPublishEvents(
+ Loan loan) {
+
+ return closeLoanWithLostItemFeesPaid(loan, loanRepository, itemRepository, eventPublisher)
+ .thenCompose(r -> r.after(eventPublisher::publishClosedLoanEvent));
+ }
+
+ private CompletableFuture> fetchLoanFeeFineData(Loan loan) {
+ return accountRepository.findAccountsForLoan(loan)
+ .thenComposeAsync(lostItemPolicyRepository::findLostItemPolicyForLoan)
+ .thenComposeAsync(actualCostRecordRepository::findByLoan);
+ }
+
+ private CompletableFuture> closeLoanWithLostItemFeesPaid(Loan loan,
+ LoanRepository loanRepository, ItemRepository itemRepository, EventPublisher eventPublisher) {
+
+ if (!shouldCloseLoan(loan)) {
+ return completedFuture(succeeded(loan));
+ }
+
+ boolean wasLoanOpen = loan.isOpen();
+ loan.closeLoanAsLostAndPaid();
+
+ return new StoreLoanAndItem(loanRepository, itemRepository).updateLoanAndItemInStorage(loan)
+ .thenCompose(r -> r.after(l -> publishLoanClosedEvent(l, wasLoanOpen, eventPublisher)));
+ }
+
+ private CompletableFuture> publishLoanClosedEvent(Loan loan, boolean wasLoanOpen,
+ EventPublisher eventPublisher) {
+
+ return wasLoanOpen && loan.isClosed()
+ ? eventPublisher.publishLoanClosedEvent(loan)
+ : completedFuture(succeeded(loan));
+ }
+
+ private boolean allLostFeesClosed(Loan loan) {
+ return loan.getAccounts().stream()
+ .filter(account -> lostItemFeeTypes().contains(account.getFeeFineType()))
+ .allMatch(Account::isClosed);
+ }
+
+ private boolean shouldCloseLoan(Loan loan) {
+ if (allLostFeesClosed(loan)) {
+ ActualCostRecord actualCostRecord = loan.getActualCostRecord();
+ if (actualCostRecord == null) {
+ return true;
+ }
+ if (loan.getAccounts().stream().noneMatch(account ->
+ LOST_ITEM_ACTUAL_COST_FEE_TYPE.equals(account.getFeeFineType()))) {
+
+ ZonedDateTime expirationDate = actualCostRecord.getExpirationDate();
+
+ return expirationDate != null && getZonedDateTime().isAfter(expirationDate);
+ } else {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/main/java/org/folio/circulation/services/LostItemFeeChargingService.java b/src/main/java/org/folio/circulation/services/LostItemFeeChargingService.java
index 3938e5939d..af08f05951 100644
--- a/src/main/java/org/folio/circulation/services/LostItemFeeChargingService.java
+++ b/src/main/java/org/folio/circulation/services/LostItemFeeChargingService.java
@@ -34,6 +34,7 @@
import org.folio.circulation.infrastructure.storage.feesandfines.FeeFineRepository;
import org.folio.circulation.infrastructure.storage.inventory.LocationRepository;
import org.folio.circulation.infrastructure.storage.loans.LostItemPolicyRepository;
+import org.folio.circulation.services.actualcostrecord.ActualCostRecordService;
import org.folio.circulation.services.support.CreateAccountCommand;
import org.folio.circulation.support.Clients;
import org.folio.circulation.support.results.Result;
diff --git a/src/main/java/org/folio/circulation/services/LostItemFeeRefundService.java b/src/main/java/org/folio/circulation/services/LostItemFeeRefundService.java
index 6c2b5065fc..9cbbdb1775 100644
--- a/src/main/java/org/folio/circulation/services/LostItemFeeRefundService.java
+++ b/src/main/java/org/folio/circulation/services/LostItemFeeRefundService.java
@@ -329,4 +329,4 @@ private Result noLoanFoundForLostItem(String itemId) {
"Item is lost however there is no aged to lost nor declared lost loan found",
"itemId", itemId));
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/folio/circulation/services/actualcostrecord/ActualCostRecordExpirationService.java b/src/main/java/org/folio/circulation/services/actualcostrecord/ActualCostRecordExpirationService.java
new file mode 100644
index 0000000000..9c55e81c30
--- /dev/null
+++ b/src/main/java/org/folio/circulation/services/actualcostrecord/ActualCostRecordExpirationService.java
@@ -0,0 +1,60 @@
+package org.folio.circulation.services.actualcostrecord;
+
+import static org.folio.circulation.domain.ItemStatus.DECLARED_LOST;
+import static org.folio.circulation.domain.representations.LoanProperties.ITEM_STATUS;
+import static org.folio.circulation.support.AsyncCoordinationUtil.allOf;
+import static org.folio.circulation.support.results.AsynchronousResult.fromFutureResult;
+import static org.folio.circulation.support.results.Result.ofAsync;
+import static org.folio.circulation.support.results.Result.succeeded;
+
+import java.util.concurrent.CompletableFuture;
+
+import org.folio.circulation.domain.Loan;
+import org.folio.circulation.domain.MultipleRecords;
+import org.folio.circulation.infrastructure.storage.inventory.ItemRepository;
+import org.folio.circulation.services.CloseLoanWithLostItemService;
+import org.folio.circulation.support.fetching.PageableFetcher;
+import org.folio.circulation.support.http.client.CqlQuery;
+import org.folio.circulation.support.results.Result;
+
+public class ActualCostRecordExpirationService {
+ private final PageableFetcher loanPageableFetcher;
+ private final CloseLoanWithLostItemService closeLoanWithLostItemService;
+ private final ItemRepository itemRepository;
+
+ public ActualCostRecordExpirationService(PageableFetcher loanPageableFetcher,
+ CloseLoanWithLostItemService closeLoanWithLostItemService, ItemRepository itemRepository) {
+
+ this.itemRepository = itemRepository;
+ this.loanPageableFetcher = loanPageableFetcher;
+ this.closeLoanWithLostItemService = closeLoanWithLostItemService;
+ }
+
+ public CompletableFuture> expireActualCostRecords() {
+ return fetchLoansForLostItemsQuery()
+ .after(query -> loanPageableFetcher.processPages(query, this::closeLoans));
+ }
+
+ private Result fetchLoansForLostItemsQuery() {
+ return CqlQuery.exactMatch(ITEM_STATUS, DECLARED_LOST.getValue());
+ }
+
+ private CompletableFuture> closeLoans(MultipleRecords expiredLoans) {
+ if (expiredLoans.isEmpty()) {
+ return ofAsync(() -> null);
+ }
+
+ return fromFutureResult(itemRepository.fetchItems(succeeded(expiredLoans))
+ .thenApply(r -> r.next(this::excludeLoansWithNonexistentItems)))
+ .flatMapFuture(loans -> allOf(loans.getRecords(),
+ closeLoanWithLostItemService::closeLoanWithLostItemFeesPaid))
+ .toCompletableFuture()
+ .thenApply(r -> r.map(ignored -> null));
+ }
+
+ private Result> excludeLoansWithNonexistentItems(
+ MultipleRecords loans) {
+
+ return succeeded(loans.filter(loan -> loan.getItem().isFound()));
+ }
+}
diff --git a/src/main/java/org/folio/circulation/services/ActualCostRecordService.java b/src/main/java/org/folio/circulation/services/actualcostrecord/ActualCostRecordService.java
similarity index 95%
rename from src/main/java/org/folio/circulation/services/ActualCostRecordService.java
rename to src/main/java/org/folio/circulation/services/actualcostrecord/ActualCostRecordService.java
index 3f126d267b..657a0d37f8 100644
--- a/src/main/java/org/folio/circulation/services/ActualCostRecordService.java
+++ b/src/main/java/org/folio/circulation/services/actualcostrecord/ActualCostRecordService.java
@@ -1,4 +1,4 @@
-package org.folio.circulation.services;
+package org.folio.circulation.services.actualcostrecord;
import java.time.ZonedDateTime;
import java.util.Map;
@@ -89,7 +89,7 @@ private ActualCostRecord buildActualCostRecord(Loan loan, FeeFineOwner feeFineOw
.withUserBarcode(loan.getUser().getBarcode())
.withLoanId(loan.getId())
.withItemLossType(itemLossType)
- .withDateOfLoss(dateOfLoss.toString())
+ .withDateOfLoss(dateOfLoss)
.withTitle(item.getTitle())
.withIdentifiers(item.getIdentifiers().collect(Collectors.toList()))
.withItemBarcode(item.getBarcode())
@@ -99,6 +99,8 @@ private ActualCostRecord buildActualCostRecord(Loan loan, FeeFineOwner feeFineOw
.withFeeFineOwnerId(feeFineOwner.getId())
.withFeeFineOwner(feeFineOwner.getOwner())
.withFeeFineTypeId(feeFine == null ? null : feeFine.getId())
- .withFeeFineType(feeFine == null ? null : feeFine.getFeeFineType());
+ .withFeeFineType(feeFine == null ? null : feeFine.getFeeFineType())
+ .withExpirationDate(loan.getLostItemPolicy()
+ .calculateFeeFineChargingPeriodExpirationDateTime(dateOfLoss));
}
}
diff --git a/src/main/java/org/folio/circulation/services/agedtolost/ChargeLostFeesWhenAgedToLostService.java b/src/main/java/org/folio/circulation/services/agedtolost/ChargeLostFeesWhenAgedToLostService.java
index e490bc4c08..ecfe9b3cf3 100644
--- a/src/main/java/org/folio/circulation/services/agedtolost/ChargeLostFeesWhenAgedToLostService.java
+++ b/src/main/java/org/folio/circulation/services/agedtolost/ChargeLostFeesWhenAgedToLostService.java
@@ -51,7 +51,7 @@
import org.folio.circulation.infrastructure.storage.loans.LoanRepository;
import org.folio.circulation.infrastructure.storage.loans.LostItemPolicyRepository;
import org.folio.circulation.infrastructure.storage.users.UserRepository;
-import org.folio.circulation.services.ActualCostRecordService;
+import org.folio.circulation.services.actualcostrecord.ActualCostRecordService;
import org.folio.circulation.services.EventPublisher;
import org.folio.circulation.services.FeeFineFacade;
import org.folio.circulation.services.support.CreateAccountCommand;
diff --git a/src/main/java/org/folio/circulation/storage/mappers/ActualCostRecordMapper.java b/src/main/java/org/folio/circulation/storage/mappers/ActualCostRecordMapper.java
index cd243d052a..8a88af09bd 100644
--- a/src/main/java/org/folio/circulation/storage/mappers/ActualCostRecordMapper.java
+++ b/src/main/java/org/folio/circulation/storage/mappers/ActualCostRecordMapper.java
@@ -7,6 +7,7 @@
import io.vertx.core.json.JsonObject;
import static org.folio.circulation.domain.representations.CallNumberComponentsRepresentation.createCallNumberComponents;
+import static org.folio.circulation.support.json.JsonPropertyFetcher.getDateTimeProperty;
import static org.folio.circulation.support.json.JsonPropertyFetcher.getNestedDateTimeProperty;
import static org.folio.circulation.support.json.JsonPropertyFetcher.getProperty;
import static org.folio.circulation.support.json.JsonPropertyWriter.write;
@@ -35,6 +36,7 @@ public static JsonObject toJson(ActualCostRecord actualCostRecord) {
write(json, "feeFineTypeId", actualCostRecord.getFeeFineTypeId());
write(json, "feeFineType", actualCostRecord.getFeeFineType());
write(json,"permanentItemLocation", actualCostRecord.getPermanentItemLocation());
+ write(json, "expirationDate", actualCostRecord.getExpirationDate());
if (actualCostRecord.getAccountId() != null) {
write(json, "accountId", actualCostRecord.getAccountId());
@@ -44,13 +46,17 @@ public static JsonObject toJson(ActualCostRecord actualCostRecord) {
}
public static ActualCostRecord toDomain(JsonObject representation) {
+ if (representation == null ) {
+ return null;
+ }
+
return new ActualCostRecord(getProperty(representation, "id"),
getProperty(representation, "accountId"),
getProperty(representation, "userId"),
getProperty(representation, "userBarcode"),
getProperty(representation, "loanId"),
ItemLossType.from(getProperty(representation, "itemLossType")),
- getProperty(representation, "dateOfLoss"),
+ getDateTimeProperty(representation, "dateOfLoss"),
getProperty(representation, "title"),
IdentifierMapper.mapIdentifiers(representation),
getProperty(representation, "itemBarcode"),
@@ -61,7 +67,8 @@ public static ActualCostRecord toDomain(JsonObject representation) {
getProperty(representation, "feeFineOwner"),
getProperty(representation, "feeFineTypeId"),
getProperty(representation, "feeFineType"),
- getNestedDateTimeProperty(representation, "metadata", "createdDate")
+ getNestedDateTimeProperty(representation, "metadata", "createdDate"),
+ getDateTimeProperty(representation, "expirationDate")
);
}
}
diff --git a/src/test/java/api/handlers/CloseDeclaredLostLoanWhenLostItemFeesAreClosedApiTests.java b/src/test/java/api/handlers/CloseDeclaredLostLoanWhenLostItemFeesAreClosedApiTests.java
index dcd0806654..d42f8c68f0 100644
--- a/src/test/java/api/handlers/CloseDeclaredLostLoanWhenLostItemFeesAreClosedApiTests.java
+++ b/src/test/java/api/handlers/CloseDeclaredLostLoanWhenLostItemFeesAreClosedApiTests.java
@@ -1,7 +1,14 @@
package api.handlers;
+import static api.support.APITestContext.getOkapiHeadersFromContext;
+import api.support.builders.AccountBuilder;
+import api.support.builders.FeefineActionsBuilder;
+import api.support.builders.LostItemFeePolicyBuilder;
import static api.support.fakes.FakePubSub.getPublishedEventsAsList;
import static api.support.fakes.PublishedEvents.byEventType;
+import static api.support.http.InterfaceUrls.scheduledActualCostExpiration;
+import static api.support.http.InterfaceUrls.scheduledAgeToLostFeeChargingUrl;
+import api.support.http.TimedTaskClient;
import static api.support.matchers.EventMatchers.isValidLoanClosedEvent;
import static api.support.matchers.ItemMatchers.isAvailable;
import static api.support.matchers.ItemMatchers.isCheckedOut;
@@ -10,7 +17,12 @@
import static api.support.matchers.JsonObjectMatcher.hasJsonPath;
import static api.support.matchers.LoanMatchers.isClosed;
import static api.support.matchers.LoanMatchers.isOpen;
+import static java.time.Clock.fixed;
+import static java.time.ZoneOffset.UTC;
import static org.folio.circulation.domain.EventType.LOAN_CLOSED;
+import static org.folio.circulation.support.utils.ClockUtil.getZonedDateTime;
+import static org.folio.circulation.support.utils.ClockUtil.setClock;
+import static org.folio.circulation.support.utils.ClockUtil.setDefaultClock;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -20,7 +32,9 @@
import java.util.List;
import java.util.UUID;
+import org.folio.circulation.domain.policy.Period;
import org.folio.circulation.support.http.client.Response;
+import org.folio.circulation.support.utils.ClockUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -33,8 +47,11 @@ class CloseDeclaredLostLoanWhenLostItemFeesAreClosedApiTests extends APITests {
private IndividualResource loan;
private IndividualResource item;
+ private final TimedTaskClient timedTaskClient = new TimedTaskClient(getOkapiHeadersFromContext());
+
@BeforeEach
public void createLoanAndDeclareItemLost() {
+ mockClockManagerToReturnDefaultDateTime();
UUID servicePointId = servicePointsFixture.cd1().getId();
useLostItemPolicy(lostItemFeePoliciesFixture.chargeFee().getId());
@@ -98,16 +115,104 @@ void shouldNotCloseLoanIfSetCostFeeIsNotClosed() {
}
@Test
- void shouldNotCloseLoanIfActualCostFeeShouldBeCharged() {
- useLostItemPolicy(lostItemFeePoliciesFixture.create(
- lostItemFeePoliciesFixture.facultyStandardPolicy()
+ void shouldCloseLoanIfActualCostFeeHasBeenPaidWithoutProcessingFee() {
+ UUID servicePointId = servicePointsFixture.cd2().getId();
+ UUID actualCostLostItemFeePolicyId = lostItemFeePoliciesFixture.create(
+ new LostItemFeePolicyBuilder().withName("test")
+ .doNotChargeProcessingFeeWhenDeclaredLost()
+ .withActualCost(10.0)
+ .withLostItemChargeFeeFine(Period.weeks(2))).getId();
+ useLostItemPolicy(actualCostLostItemFeePolicyId);
+
+ item = itemsFixture.basedUponNod();
+ loan = checkOutFixture.checkOutByBarcode(item, usersFixture.jessica());
+ declareLostFixtures.declareItemLost(new DeclareItemLostRequestBuilder()
+ .withServicePointId(servicePointId)
+ .forLoanId(loan.getId()));
+ createLostItemFeeActualCostAccount(10.0);
+
+ feeFineAccountFixture.payLostItemActualCostFee(loan.getId());
+ eventSubscribersFixture.publishLoanRelatedFeeFineClosedEvent(loan.getId());
+
+ assertThat(loansFixture.getLoanById(loan.getId()).getJson(), isClosed());
+ assertThat(itemsClient.getById(item.getId()).getJson(), isLostAndPaid());
+ }
+
+ @Test
+ void shouldCloseLoanIfActualCostFeeAndProcessingFeeHaveBeenPaid() {
+ UUID servicePointId = servicePointsFixture.cd2().getId();
+ UUID actualCostLostItemFeePolicyId = lostItemFeePoliciesFixture.create(
+ new LostItemFeePolicyBuilder().withName("test")
.chargeProcessingFeeWhenDeclaredLost(10.00)
- .withActualCost(10.0)).getId());
+ .withActualCost(10.0)
+ .withLostItemChargeFeeFine(Period.weeks(2))).getId();
+ useLostItemPolicy(actualCostLostItemFeePolicyId);
+ item = itemsFixture.basedUponNod();
+ loan = checkOutFixture.checkOutByBarcode(item, usersFixture.jessica());
+ declareLostFixtures.declareItemLost(new DeclareItemLostRequestBuilder()
+ .withServicePointId(servicePointId)
+ .forLoanId(loan.getId()));
+ createLostItemFeeActualCostAccount(10.0);
+
+ feeFineAccountFixture.payLostItemActualCostFee(loan.getId());
feeFineAccountFixture.payLostItemProcessingFee(loan.getId());
+ eventSubscribersFixture.publishLoanRelatedFeeFineClosedEvent(loan.getId());
+
+ assertThat(loansFixture.getLoanById(loan.getId()).getJson(), isClosed());
+ assertThat(itemsClient.getById(item.getId()).getJson(), isLostAndPaid());
+ }
+
+ @Test
+ void shouldCloseLoanIfActualCostFeeShouldBeChargedButChargingPeriodElapsedAndProcessingFeeHasBeenPaid() {
+ UUID servicePointId = servicePointsFixture.cd2().getId();
+
+ UUID actualCostLostItemFeePolicyId = lostItemFeePoliciesFixture.create(
+ new LostItemFeePolicyBuilder().withName("test")
+ .chargeProcessingFeeWhenDeclaredLost(10.00)
+ .withActualCost(10.0)
+ .withLostItemChargeFeeFine(Period.weeks(2))).getId();
+ useLostItemPolicy(actualCostLostItemFeePolicyId);
+ item = itemsFixture.basedUponNod();
+ loan = checkOutFixture.checkOutByBarcode(item, usersFixture.jessica());
+ declareLostFixtures.declareItemLost(new DeclareItemLostRequestBuilder()
+ .withServicePointId(servicePointId)
+ .forLoanId(loan.getId()));
+
+ mockClockManagerToReturnFixedDateTime(ClockUtil.getZonedDateTime().plusWeeks(3));
+ feeFineAccountFixture.payLostItemProcessingFee(loan.getId());
eventSubscribersFixture.publishLoanRelatedFeeFineClosedEvent(loan.getId());
+ assertThat(loansFixture.getLoanById(loan.getId()).getJson(), isClosed());
+ assertThat(itemsClient.getById(item.getId()).getJson(), isLostAndPaid());
+ }
+
+ @Test
+ void shouldNotCloseLoanDuringScheduledExpirationIfChargingPeriodHasNotElapsedAndProcessingFeeHasBeenPaid() {
+ UUID servicePointId = servicePointsFixture.cd2().getId();
+
+ UUID actualCostLostItemFeePolicyId = lostItemFeePoliciesFixture.create(
+ new LostItemFeePolicyBuilder().withName("test")
+ .chargeProcessingFeeWhenDeclaredLost(10.00)
+ .withActualCost(10.0)
+ .withLostItemChargeFeeFine(Period.weeks(2))).getId();
+ useLostItemPolicy(actualCostLostItemFeePolicyId);
+
+ item = itemsFixture.basedUponNod();
+ loan = checkOutFixture.checkOutByBarcode(item, usersFixture.jessica());
+ declareLostFixtures.declareItemLost(new DeclareItemLostRequestBuilder()
+ .withServicePointId(servicePointId)
+ .forLoanId(loan.getId()));
+
+ feeFineAccountFixture.payLostItemProcessingFee(loan.getId());
+ eventSubscribersFixture.publishLoanRelatedFeeFineClosedEvent(loan.getId());
+
+ mockClockManagerToReturnFixedDateTime(ClockUtil.getZonedDateTime().plusWeeks(1));
+
+ timedTaskClient.start(scheduledActualCostExpiration(), 204,
+ "scheduled-actual-cost-expiration");
+
assertThat(loansFixture.getLoanById(loan.getId()).getJson(), isOpen());
assertThat(itemsClient.getById(item.getId()).getJson(), isDeclaredLost());
}
@@ -177,4 +282,23 @@ void shouldNotPublishLoanClosedEventWhenLoanIsOriginallyClosed() {
assertThat(getPublishedEventsAsList(byEventType(LOAN_CLOSED)), empty());
}
+
+ protected void createLostItemFeeActualCostAccount(double amount) {
+ IndividualResource account = accountsClient.create(new AccountBuilder()
+ .withLoan(loan)
+ .withAmount(amount)
+ .withRemainingFeeFine(amount)
+ .feeFineStatusOpen()
+ .withFeeFineActualCostType()
+ .withFeeFine(feeFineTypeFixture.lostItemActualCostFee())
+ .withOwner(feeFineOwnerFixture.cd1Owner())
+ .withPaymentStatus("Outstanding"));
+
+ feeFineActionsClient.create(new FeefineActionsBuilder()
+ .forAccount(account.getId())
+ .withBalance(amount)
+ .withActionAmount(amount)
+ .withActionType("Lost item fee (actual cost)"));
+ }
+
}
diff --git a/src/test/java/api/loans/DeclareLostAPITests.java b/src/test/java/api/loans/DeclareLostAPITests.java
index b6b3e4e697..5065f48aad 100644
--- a/src/test/java/api/loans/DeclareLostAPITests.java
+++ b/src/test/java/api/loans/DeclareLostAPITests.java
@@ -827,56 +827,56 @@ public void shouldRefundPartiallyPaidOrTransferredLostItemFeesBeforeApplyingNewF
@Test
void shouldClearExistingFeesAndCloseLoanAsLostAndPaidIfLostandPaidItemDeclaredLostAndPolicySetNotToChargeFees() {
- final double lostItemProcessingFee = 20.0;
- UUID servicePointId = servicePointsFixture.cd1().getId();
+ final double lostItemProcessingFee = 20.0;
+ UUID servicePointId = servicePointsFixture.cd1().getId();
- final LostItemFeePolicyBuilder lostPolicyBuilder = lostItemFeePoliciesFixture.ageToLostAfterOneMinutePolicy()
- .withName("age to lost with processing fees")
- .billPatronImmediatelyWhenAgedToLost()
- .withNoFeeRefundInterval()
- .withNoChargeAmountItem()
- .doNotChargeProcessingFeeWhenDeclaredLost()
- .chargeProcessingFeeWhenAgedToLost(lostItemProcessingFee);
+ final LostItemFeePolicyBuilder lostPolicyBuilder = lostItemFeePoliciesFixture.ageToLostAfterOneMinutePolicy()
+ .withName("age to lost with processing fees")
+ .billPatronImmediatelyWhenAgedToLost()
+ .withNoFeeRefundInterval()
+ .withNoChargeAmountItem()
+ .doNotChargeProcessingFeeWhenDeclaredLost()
+ .chargeProcessingFeeWhenAgedToLost(lostItemProcessingFee);
- useLostItemPolicy(lostItemFeePoliciesFixture.create(lostPolicyBuilder).getId());
+ useLostItemPolicy(lostItemFeePoliciesFixture.create(lostPolicyBuilder).getId());
- AgeToLostResult agedToLostResult = ageToLostFixture.createLoanAgeToLostAndChargeFees(lostPolicyBuilder);
- UUID testLoanId = agedToLostResult.getLoanId();
- UUID itemId = agedToLostResult.getItemId();
+ AgeToLostResult agedToLostResult = ageToLostFixture.createLoanAgeToLostAndChargeFees(lostPolicyBuilder);
+ UUID testLoanId = agedToLostResult.getLoanId();
+ UUID itemId = agedToLostResult.getItemId();
- JsonObject AgeToLostItem = itemsFixture.getById(itemId).getJson();
+ JsonObject AgeToLostItem = itemsFixture.getById(itemId).getJson();
- assertThat(AgeToLostItem, isAgedToLost());
+ assertThat(AgeToLostItem, isAgedToLost());
- JsonObject itemFee = getAccountForLoan(testLoanId, "Lost item processing fee");
+ JsonObject itemFee = getAccountForLoan(testLoanId, "Lost item processing fee");
- assertThat(itemFee, hasJsonPath("amount", lostItemProcessingFee));
+ assertThat(itemFee, hasJsonPath("amount", lostItemProcessingFee));
- final ZonedDateTime declareLostDate = getZonedDateTime().plusWeeks(1);
- mockClockManagerToReturnFixedDateTime(declareLostDate);
+ final ZonedDateTime declareLostDate = getZonedDateTime().plusWeeks(1);
+ mockClockManagerToReturnFixedDateTime(declareLostDate);
- final DeclareItemLostRequestBuilder builder = new DeclareItemLostRequestBuilder()
- .forLoanId(testLoanId)
- .withServicePointId(servicePointId)
- .on(declareLostDate)
- .withNoComment();
+ final DeclareItemLostRequestBuilder builder = new DeclareItemLostRequestBuilder()
+ .forLoanId(testLoanId)
+ .withServicePointId(servicePointId)
+ .on(declareLostDate)
+ .withNoComment();
- FakePubSub.clearPublishedEvents();
+ FakePubSub.clearPublishedEvents();
- declareLostFixtures.declareItemLost(builder);
+ declareLostFixtures.declareItemLost(builder);
- JsonObject declareLostLoan = loansClient.getById(testLoanId).getJson();
- JsonObject declareLostItem = itemsFixture.getById(itemId).getJson();
+ JsonObject declareLostLoan = loansClient.getById(testLoanId).getJson();
+ JsonObject declareLostItem = itemsFixture.getById(itemId).getJson();
- assertThat(declareLostItem, isLostAndPaid());
+ assertThat(declareLostItem, isLostAndPaid());
- Double finalAmountRemaining = declareLostLoan.getJsonObject("feesAndFines").getDouble("amountRemainingToPay");
- assertEquals(finalAmountRemaining, 0.0, 0.01);
+ Double finalAmountRemaining = declareLostLoan.getJsonObject("feesAndFines").getDouble("amountRemainingToPay");
+ assertEquals(finalAmountRemaining, 0.0, 0.01);
- List accounts = getAccountsForLoan(testLoanId);
+ List accounts = getAccountsForLoan(testLoanId);
- assertThat(accounts, hasSize(1));
- assertThat(getOpenAccounts(accounts), hasSize(0));
+ assertThat(accounts, hasSize(1));
+ assertThat(getOpenAccounts(accounts), hasSize(0));
verifyNumberOfPublishedEvents(LOAN_CLOSED, 1);
verifyNumberOfPublishedEvents(ITEM_DECLARED_LOST, 0);
diff --git a/src/test/java/api/support/builders/AccountBuilder.java b/src/test/java/api/support/builders/AccountBuilder.java
index 1416c79e4f..c9c1da4a05 100644
--- a/src/test/java/api/support/builders/AccountBuilder.java
+++ b/src/test/java/api/support/builders/AccountBuilder.java
@@ -1,15 +1,11 @@
package api.support.builders;
-
-
import static org.folio.circulation.support.json.JsonPropertyWriter.write;
-
import java.util.UUID;
import api.support.http.IndividualResource;
import io.vertx.core.json.JsonObject;
public class AccountBuilder extends JsonBuilder implements Builder {
-
private String id;
private String loanId;
private Double remainingAmount;
@@ -40,7 +36,6 @@ public AccountBuilder() {
@Override
public JsonObject create() {
JsonObject accountRequest = new JsonObject();
-
write(accountRequest, "id", id);
write(accountRequest, "loanId", loanId);
write(accountRequest, "amount", amount);
diff --git a/src/test/java/api/support/builders/LostItemFeePolicyBuilder.java b/src/test/java/api/support/builders/LostItemFeePolicyBuilder.java
index a91787994d..64bcb5d077 100644
--- a/src/test/java/api/support/builders/LostItemFeePolicyBuilder.java
+++ b/src/test/java/api/support/builders/LostItemFeePolicyBuilder.java
@@ -24,7 +24,7 @@ public class LostItemFeePolicyBuilder extends JsonBuilder implements Builder {
private final Double lostItemProcessingFee;
private final boolean chargeAmountItemPatron;
private final boolean chargeAmountItemSystem;
- private final JsonObject lostItemChargeFeeFine;
+ private final Period lostItemChargeFeeFine;
private final boolean returnedLostItemProcessingFee;
private final boolean replacedLostItemProcessingFee;
private final double replacementProcessingFee;
@@ -44,7 +44,7 @@ public LostItemFeePolicyBuilder() {
null,
false,
false,
- new JsonObject(),
+ null,
false,
false,
0.0,
@@ -156,13 +156,16 @@ public JsonObject create() {
request.put("feesFinesShallRefunded", this.feeRefundInterval.asJson());
}
+ if (lostItemChargeFeeFine != null) {
+ request.put("lostItemChargeFeeFine", this.lostItemChargeFeeFine.asJson());
+ }
+
put(request, "name", this.name);
put(request, "description", this.description);
put(request, "chargeAmountItem", this.chargeAmountItem);
put(request, "lostItemProcessingFee", this.lostItemProcessingFee);
put(request, "chargeAmountItemPatron", this.chargeAmountItemPatron);
put(request, "chargeAmountItemSystem", this.chargeAmountItemSystem);
- put(request, "lostItemChargeFeeFine", this.lostItemChargeFeeFine);
put(request, "returnedLostItemProcessingFee", this.returnedLostItemProcessingFee);
put(request, "replacedLostItemProcessingFee", this.replacedLostItemProcessingFee);
put(request, "replacementProcessingFee", String.valueOf(this.replacementProcessingFee));
diff --git a/src/test/java/api/support/fixtures/FeeFineAccountFixture.java b/src/test/java/api/support/fixtures/FeeFineAccountFixture.java
index 56d8431e50..7343596548 100644
--- a/src/test/java/api/support/fixtures/FeeFineAccountFixture.java
+++ b/src/test/java/api/support/fixtures/FeeFineAccountFixture.java
@@ -12,6 +12,9 @@
import api.support.builders.FeefineActionsBuilder;
import api.support.http.ResourceClient;
import io.vertx.core.json.JsonObject;
+import static org.folio.circulation.domain.FeeFine.LOST_ITEM_ACTUAL_COST_FEE_TYPE;
+import static org.folio.circulation.domain.FeeFine.LOST_ITEM_FEE_TYPE;
+import static org.folio.circulation.domain.FeeFine.LOST_ITEM_PROCESSING_FEE_TYPE;
public final class FeeFineAccountFixture {
private final ResourceClient accountsClient = forAccounts();
@@ -77,6 +80,13 @@ public void payLostItemActualCostFee(UUID loanId, double amount) {
pay(accountId, amount);
}
+ public void payLostItemActualCostFee(UUID loanId) {
+ final JsonObject lostItemFeeActualCostAccount = getAccount(loanId, LOST_ITEM_ACTUAL_COST_FEE_TYPE);
+ final String accountId = lostItemFeeActualCostAccount.getString("id");
+
+ pay(accountId, lostItemFeeActualCostAccount.getDouble("amount"));
+ }
+
public void payLostItemProcessingFee(UUID loanId) {
final JsonObject lostItemProcessingFeeAccount = getAccount(
loanId, LOST_ITEM_PROCESSING_FEE_TYPE);
@@ -131,6 +141,13 @@ public IndividualResource createManualFeeForLoan(IndividualResource loan, double
return account;
}
+ private JsonObject getLostItemFeeAccount(UUID loanId) {
+ return accountsClient.getMany(exactMatch("loanId", loanId.toString())
+ .and(exactMatch("feeFineType", "Lost item fee")))
+ .getFirst();
+ }
+
+
private JsonObject getAccount(UUID loanId, String feeFineType) {
return accountsClient.getMany(exactMatch("loanId", loanId.toString())
.and(exactMatch("feeFineType", feeFineType)))
diff --git a/src/test/java/api/support/fixtures/LostItemFeePoliciesFixture.java b/src/test/java/api/support/fixtures/LostItemFeePoliciesFixture.java
index 204860cb55..db6bca4683 100644
--- a/src/test/java/api/support/fixtures/LostItemFeePoliciesFixture.java
+++ b/src/test/java/api/support/fixtures/LostItemFeePoliciesFixture.java
@@ -2,6 +2,7 @@
import static api.support.http.ResourceClient.forLostItemFeePolicies;
import static org.folio.circulation.domain.policy.Period.minutes;
+import static org.folio.circulation.domain.policy.lostitem.ChargeAmountType.ACTUAL_COST;
import static org.folio.circulation.domain.policy.lostitem.ChargeAmountType.SET_COST;
import static org.folio.circulation.support.json.JsonPropertyFetcher.getProperty;
@@ -34,19 +35,19 @@ public IndividualResource facultyStandard() {
public IndividualResource chargeFee() {
createReferenceData();
- return create(chargeFeePolicy(10.0, 5.0));
+ return create(chargeSetCostFeePolicy(10.0, 5.0));
}
public IndividualResource chargeFeeWithZeroLostItemFee() {
createReferenceData();
- return create(chargeFeePolicy(0.0, 5.0));
+ return create(chargeSetCostFeePolicy(0.0, 5.0));
}
public IndividualResource chargeFeeWithZeroLostItemProcessingFee() {
createReferenceData();
- return create(chargeFeePolicy(10.0, 0.0));
+ return create(chargeSetCostFeePolicy(10.0, 0.0));
}
public IndividualResource ageToLostAfterOneMinute() {
@@ -86,12 +87,18 @@ public LostItemFeePolicyBuilder facultyStandardPolicy() {
.chargeOverdueFineWhenReturned();
}
- private LostItemFeePolicyBuilder chargeFeePolicy(double lostItemFeeCost,
+ private LostItemFeePolicyBuilder chargeSetCostFeePolicy(double lostItemFeeCost,
double lostItemProcessingFeeCost) {
return chargeFeePolicy(lostItemFeeCost, lostItemProcessingFeeCost, SET_COST);
}
+ private LostItemFeePolicyBuilder chargeActualCostFeePolicy(double lostItemFeeCost,
+ double lostItemProcessingFeeCost) {
+
+ return chargeFeePolicy(lostItemFeeCost, lostItemProcessingFeeCost, ACTUAL_COST);
+ }
+
private LostItemFeePolicyBuilder chargeFeePolicy(double lostItemFeeCost,
double lostItemProcessingFeeCost, ChargeAmountType costType) {
diff --git a/src/test/java/api/support/http/InterfaceUrls.java b/src/test/java/api/support/http/InterfaceUrls.java
index 11bcce7759..793ee2d787 100644
--- a/src/test/java/api/support/http/InterfaceUrls.java
+++ b/src/test/java/api/support/http/InterfaceUrls.java
@@ -297,6 +297,10 @@ public static URL scheduledAgeToLostFeeChargingUrl() {
return circulationModuleUrl("/circulation/scheduled-age-to-lost-fee-charging");
}
+ public static URL scheduledActualCostExpiration() {
+ return circulationModuleUrl("/circulation/actual-cost-expiration-by-timeout");
+ }
+
public static URL actualCostRecordsStorageUrl(String subPath) {
return APITestContext.viaOkapiModuleUrl("/actual-cost-record-storage/actual-cost-records" + subPath);
}