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 @@
     <log4j2.version>2.17.1</log4j2.version>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
-    <lombok.version>1.18.16</lombok.version>
+    <lombok.version>1.18.22</lombok.version>
     <spring.version>5.2.22.RELEASE</spring.version>
     <maven.site.plugin.version>3.9.1</maven.site.plugin.version>
   </properties>
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<Void> 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<Identifier> 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<Account> 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<Account> 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<Result<ActualCostRecord>> getActualCostRecordByAccountI
   }
 
   private Result<MultipleRecords<ActualCostRecord>> mapResponseToActualCostRecords(Response response) {
-    return MultipleRecords.from(response, ActualCostRecordMapper::toDomain, "actualCostRecords");
+    return MultipleRecords.from(response, ActualCostRecordMapper::toDomain, ACTUAL_COST_RECORDS);
+  }
+
+  public CompletableFuture<Result<Loan>> findByLoan(Result<Loan> 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<Result<MultipleRecords<Loan>>> fetchActualCostRecords(
+    MultipleRecords<Loan> 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<Result<Map<String, ActualCostRecord>>> buildLoanIdToActualCostRecordMap(
+    Collection<Loan> loans) {
+
+    final Set<String> 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<JsonObject> 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<Result<Item>> fetchItemByBarcode(String barcode) {
   }
 
   public <T extends ItemRelatedRecord> CompletableFuture<Result<MultipleRecords<T>>>
-  fetchItemsWithHoldings(Result<MultipleRecords<T>> result, BiFunction<T, Item, T> includeItemMap) {
+  fetchItemsWithHoldings(Result<MultipleRecords<T>> result, BiFunction<T, Item, T> withItemMapper) {
 
-    return fetchItemsFor(result, includeItemMap, this::fetchItemsWithHoldingsRecords);
+    return fetchItemsFor(result, withItemMapper, this::fetchItems);
+  }
+
+  public <T extends ItemRelatedRecord> CompletableFuture<Result<MultipleRecords<T>>>
+  fetchItems(Result<MultipleRecords<T>> result) {
+
+    return fetchItemsFor(result, (itemRelatedRecord, item) -> (T) itemRelatedRecord.withItem(item),
+      this::fetchItems);
   }
 
   public <T extends ItemRelatedRecord> CompletableFuture<Result<MultipleRecords<T>>>
@@ -309,13 +316,6 @@ private CompletableFuture<Result<MultipleRecords<Item>>> fetchFor(
       .thenComposeAsync(this::fetchItemsRelatedRecords);
   }
 
-  private CompletableFuture<Result<MultipleRecords<Item>>> fetchItemsWithHoldingsRecords(
-    Collection<String> itemIds) {
-
-    return fetchItems(itemIds)
-      .thenComposeAsync(this::fetchHoldingsRecords);
-  }
-
   public CompletableFuture<Result<Item>> fetchItemRelatedRecords(Result<Item> 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<Result<Loan>> 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<Result<Void>> 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<Result<Loan>> 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<Result<Loan>> 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<Result<Loan>> 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<LoanRelatedFeeFineClosedEvent> 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<Result<Void>> closeLoanWithLostItemFeesPaid(Loan loan) {
+    if (loan == null || !loan.isItemLost()) {
+      return completedFuture(Result.succeeded(null));
+    }
+
+    return fetchLoanFeeFineData(loan)
+      .thenCompose(r -> r.after(this::closeLoanWithLostItemFeesPaidAndPublishEvents));
+  }
+
+  private CompletableFuture<Result<Void>> closeLoanWithLostItemFeesPaidAndPublishEvents(
+    Loan loan) {
+
+    return closeLoanWithLostItemFeesPaid(loan, loanRepository, itemRepository, eventPublisher)
+      .thenCompose(r -> r.after(eventPublisher::publishClosedLoanEvent));
+  }
+
+  private CompletableFuture<Result<Loan>> fetchLoanFeeFineData(Loan loan) {
+    return accountRepository.findAccountsForLoan(loan)
+      .thenComposeAsync(lostItemPolicyRepository::findLostItemPolicyForLoan)
+      .thenComposeAsync(actualCostRecordRepository::findByLoan);
+  }
+
+  private CompletableFuture<Result<Loan>> 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<Result<Loan>> 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<LostItemFeeRefundContext> 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<Loan> loanPageableFetcher;
+  private final CloseLoanWithLostItemService closeLoanWithLostItemService;
+  private final ItemRepository itemRepository;
+
+  public ActualCostRecordExpirationService(PageableFetcher<Loan> loanPageableFetcher,
+    CloseLoanWithLostItemService closeLoanWithLostItemService, ItemRepository itemRepository) {
+
+    this.itemRepository = itemRepository;
+    this.loanPageableFetcher = loanPageableFetcher;
+    this.closeLoanWithLostItemService = closeLoanWithLostItemService;
+  }
+
+  public CompletableFuture<Result<Void>> expireActualCostRecords() {
+    return fetchLoansForLostItemsQuery()
+      .after(query -> loanPageableFetcher.processPages(query, this::closeLoans));
+  }
+
+  private Result<CqlQuery> fetchLoansForLostItemsQuery() {
+    return CqlQuery.exactMatch(ITEM_STATUS, DECLARED_LOST.getValue());
+  }
+
+  private CompletableFuture<Result<Void>> closeLoans(MultipleRecords<Loan> 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<MultipleRecords<Loan>> excludeLoansWithNonexistentItems(
+    MultipleRecords<Loan> 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<JsonObject> accounts = getAccountsForLoan(testLoanId);
+    List<JsonObject> 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);
   }