From 8f75807584d896f6844bb6182d3836f39a4c7d10 Mon Sep 17 00:00:00 2001
From: obozhko-folio <83696213+obozhko-folio@users.noreply.github.com>
Date: Thu, 26 Sep 2024 15:13:49 +0300
Subject: [PATCH] MODBULKOPS-334 - Preventing record update with values from
 different tenants  (#264)

* MODBULKOPS-334 Added validation
---
 .../entity/BulkOperationRuleDetails.java      |   2 +
 .../RuleValidationTenantsException.java       |   7 +
 .../processor/AbstractDataProcessor.java      |  33 ++-
 .../processor/FolioInstanceDataProcessor.java |   7 +-
 .../processor/HoldingsDataProcessor.java      |  22 +-
 .../bulkops/processor/ItemDataProcessor.java  |  22 +-
 .../bulkops/processor/UserDataProcessor.java  |   7 +-
 .../folio/bulkops/processor/Validator.java    |   5 +-
 .../bulkops/service/ConsortiaService.java     |   4 +-
 .../folio/bulkops/service/RuleService.java    |   4 +-
 .../org/folio/bulkops/util/Constants.java     |   1 +
 .../db/changelog/changelog-master.xml         |   1 +
 .../20-09-2024_add_tenants_to_rules.sql       |   2 +
 .../20-09-2024_add_tenants_to_rules.xml       |  13 +
 .../resources/swagger.api/schemas/action.json |   6 +
 .../schemas/bulk_operation_rule.json          |   6 +
 src/test/java/org/folio/bulkops/BaseTest.java |   8 +
 .../FolioInstanceDataProcessorTest.java       |   4 +-
 .../processor/HoldingsDataProcessorTest.java  | 179 ++++++++++----
 .../processor/ItemDataProcessorTest.java      | 228 ++++++++++++------
 20 files changed, 419 insertions(+), 142 deletions(-)
 create mode 100644 src/main/java/org/folio/bulkops/exception/RuleValidationTenantsException.java
 create mode 100644 src/main/resources/db/changelog/changes/20-09-2024_add_tenants_to_rules.sql
 create mode 100644 src/main/resources/db/changelog/changes/20-09-2024_add_tenants_to_rules.xml

diff --git a/src/main/java/org/folio/bulkops/domain/entity/BulkOperationRuleDetails.java b/src/main/java/org/folio/bulkops/domain/entity/BulkOperationRuleDetails.java
index 913a2eab..c3374016 100644
--- a/src/main/java/org/folio/bulkops/domain/entity/BulkOperationRuleDetails.java
+++ b/src/main/java/org/folio/bulkops/domain/entity/BulkOperationRuleDetails.java
@@ -45,4 +45,6 @@ public class BulkOperationRuleDetails {
   @Type(JsonBinaryType.class)
   @Column(columnDefinition = "jsonb")
   private List<Parameter> parameters;
+
+  private List<String> tenants;
 }
diff --git a/src/main/java/org/folio/bulkops/exception/RuleValidationTenantsException.java b/src/main/java/org/folio/bulkops/exception/RuleValidationTenantsException.java
new file mode 100644
index 00000000..0dab065f
--- /dev/null
+++ b/src/main/java/org/folio/bulkops/exception/RuleValidationTenantsException.java
@@ -0,0 +1,7 @@
+package org.folio.bulkops.exception;
+
+public class RuleValidationTenantsException extends Exception {
+  public RuleValidationTenantsException(String message) {
+    super(message);
+  }
+}
diff --git a/src/main/java/org/folio/bulkops/processor/AbstractDataProcessor.java b/src/main/java/org/folio/bulkops/processor/AbstractDataProcessor.java
index 8166668f..902222f2 100644
--- a/src/main/java/org/folio/bulkops/processor/AbstractDataProcessor.java
+++ b/src/main/java/org/folio/bulkops/processor/AbstractDataProcessor.java
@@ -8,17 +8,30 @@
 import org.folio.bulkops.domain.dto.BulkOperationRuleCollection;
 import org.folio.bulkops.domain.dto.UpdateOptionType;
 import org.folio.bulkops.exception.RuleValidationException;
+import org.folio.bulkops.exception.RuleValidationTenantsException;
+import org.folio.bulkops.service.ConsortiaService;
 import org.folio.bulkops.service.ErrorService;
+import org.folio.spring.FolioExecutionContext;
+import org.folio.spring.FolioModuleMetadata;
+import org.folio.spring.scope.FolioExecutionContextSetter;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import lombok.extern.log4j.Log4j2;
 
+import static org.folio.bulkops.util.FolioExecutionContextUtil.prepareContextForTenant;
+
 @Log4j2
 @Component
 public abstract class AbstractDataProcessor<T extends BulkOperationsEntity> implements DataProcessor<T> {
   @Autowired
   private ErrorService errorService;
+  @Autowired
+  private FolioModuleMetadata folioModuleMetadata;
+  @Autowired
+  private ConsortiaService consortiaService;
+  @Autowired
+  protected FolioExecutionContext folioExecutionContext;
 
   @Override
   public UpdatedEntityHolder process(String identifier, T entity, BulkOperationRuleCollection rules) {
@@ -30,11 +43,17 @@ public UpdatedEntityHolder process(String identifier, T entity, BulkOperationRul
       var option = details.getOption();
       for (Action action : details.getActions()) {
         try {
-          updater(option, action).apply(preview);
-          validator(entity).validate(option, action);
-          updater(option, action).apply(updated);
+          updater(option, action, entity, rule).apply(preview);
+          validator(entity).validate(option, action, rule);
+          updater(option, action, entity, rule).apply(updated);
         } catch (RuleValidationException e) {
           errorService.saveError(rule.getBulkOperationId(), identifier, e.getMessage());
+        } catch (RuleValidationTenantsException e) {
+          try (var ignored = new FolioExecutionContextSetter(prepareContextForTenant(consortiaService.getCentralTenantId(folioExecutionContext.getTenantId()), folioModuleMetadata, folioExecutionContext))) {
+            log.info("current tenant: {}", folioExecutionContext.getTenantId());
+            errorService.saveError(rule.getBulkOperationId(), identifier, e.getMessage());
+          }
+          log.error(e.getMessage());
         } catch (Exception e) {
           log.error(String.format("%s id=%s, error: %s", updated.getRecordBulkOperationEntity().getClass().getSimpleName(), "id", e.getMessage()));
           errorService.saveError(rule.getBulkOperationId(), identifier, e.getMessage());
@@ -50,18 +69,20 @@ public UpdatedEntityHolder process(String identifier, T entity, BulkOperationRul
    * Returns validator
    *
    * @param entity entity of type {@link T} to validate
-   * @return true if {@link UpdateOptionType} and {@link Action} can be applied to entity
+   * @return true if {@link UpdateOptionType} and {@link Action}, and {@link BulkOperationRule} can be applied to entity
    */
-  public abstract Validator<UpdateOptionType, Action> validator(T entity);
+  public abstract Validator<UpdateOptionType, Action, BulkOperationRule> validator(T entity);
 
   /**
    * Returns {@link Consumer<T>} for applying changes for entity of type {@link T}
    *
    * @param option {@link UpdateOptionType} for update
    * @param action {@link Action} for update
+   * @param action {@link T} for update
+   * @param action {@link BulkOperationRule} for update
    * @return updater
    */
-  public abstract Updater<T> updater(UpdateOptionType option, Action action);
+  public abstract Updater<T> updater(UpdateOptionType option, Action action, T entity, BulkOperationRule rule) throws RuleValidationTenantsException;
 
   /**
    * Clones object of type {@link T}
diff --git a/src/main/java/org/folio/bulkops/processor/FolioInstanceDataProcessor.java b/src/main/java/org/folio/bulkops/processor/FolioInstanceDataProcessor.java
index 83b78dc2..c1a4aa50 100644
--- a/src/main/java/org/folio/bulkops/processor/FolioInstanceDataProcessor.java
+++ b/src/main/java/org/folio/bulkops/processor/FolioInstanceDataProcessor.java
@@ -16,6 +16,7 @@
 import org.folio.bulkops.domain.bean.ExtendedInstance;
 import org.folio.bulkops.domain.dto.Action;
 import org.folio.bulkops.domain.dto.UpdateOptionType;
+import org.folio.bulkops.domain.dto.BulkOperationRule;
 import org.folio.bulkops.exception.BulkOperationException;
 import org.folio.bulkops.exception.RuleValidationException;
 import org.springframework.stereotype.Component;
@@ -32,8 +33,8 @@ public class FolioInstanceDataProcessor extends AbstractDataProcessor<ExtendedIn
   private final InstanceNotesUpdaterFactory instanceNotesUpdaterFactory;
 
   @Override
-  public Validator<UpdateOptionType, Action> validator(ExtendedInstance extendedInstance) {
-    return (option, action) -> {
+  public Validator<UpdateOptionType, Action, BulkOperationRule> validator(ExtendedInstance extendedInstance) {
+    return (option, action, rule) -> {
       if (CLEAR_FIELD.equals(action.getType()) && Set.of(STAFF_SUPPRESS, SUPPRESS_FROM_DISCOVERY).contains(option)) {
         throw new RuleValidationException("Suppress flag cannot be cleared.");
       } else if (INSTANCE_NOTE.equals(option) && !"FOLIO".equals(extendedInstance.getEntity().getSource())) {
@@ -45,7 +46,7 @@ public Validator<UpdateOptionType, Action> validator(ExtendedInstance extendedIn
   }
 
   @Override
-  public Updater<ExtendedInstance> updater(UpdateOptionType option, Action action) {
+  public Updater<ExtendedInstance> updater(UpdateOptionType option, Action action, ExtendedInstance entity, BulkOperationRule rule) {
     if (STAFF_SUPPRESS.equals(option)) {
       if (SET_TO_TRUE.equals(action.getType())) {
         return extendedInstance -> extendedInstance.getEntity().setStaffSuppress(true);
diff --git a/src/main/java/org/folio/bulkops/processor/HoldingsDataProcessor.java b/src/main/java/org/folio/bulkops/processor/HoldingsDataProcessor.java
index f7b48c65..cbf35e59 100644
--- a/src/main/java/org/folio/bulkops/processor/HoldingsDataProcessor.java
+++ b/src/main/java/org/folio/bulkops/processor/HoldingsDataProcessor.java
@@ -1,6 +1,7 @@
 package org.folio.bulkops.processor;
 
 import static java.lang.String.format;
+import static java.util.Objects.nonNull;
 import static org.apache.commons.lang3.ObjectUtils.isEmpty;
 import static org.folio.bulkops.domain.dto.UpdateActionType.CLEAR_FIELD;
 import static org.folio.bulkops.domain.dto.UpdateActionType.REPLACE_WITH;
@@ -16,6 +17,7 @@
 import static org.folio.bulkops.domain.dto.UpdateOptionType.PERMANENT_LOCATION;
 import static org.folio.bulkops.domain.dto.UpdateOptionType.SUPPRESS_FROM_DISCOVERY;
 import static org.folio.bulkops.domain.dto.UpdateOptionType.TEMPORARY_LOCATION;
+import static org.folio.bulkops.util.Constants.RECORD_CANNOT_BE_UPDATED_ERROR_TEMPLATE;
 
 import java.util.ArrayList;
 import java.util.Objects;
@@ -25,15 +27,16 @@
 import org.apache.commons.lang3.StringUtils;
 import org.folio.bulkops.domain.bean.ExtendedHoldingsRecord;
 import org.folio.bulkops.domain.dto.Action;
+import org.folio.bulkops.domain.dto.BulkOperationRule;
 import org.folio.bulkops.domain.dto.UpdateActionType;
 import org.folio.bulkops.domain.dto.UpdateOptionType;
 import org.folio.bulkops.exception.BulkOperationException;
 import org.folio.bulkops.exception.NotFoundException;
 import org.folio.bulkops.exception.RuleValidationException;
+import org.folio.bulkops.exception.RuleValidationTenantsException;
 import org.folio.bulkops.service.HoldingsReferenceService;
 import org.folio.bulkops.service.ItemReferenceService;
 import org.folio.bulkops.service.ElectronicAccessReferenceService;
-import org.folio.spring.FolioExecutionContext;
 import org.springframework.stereotype.Component;
 
 import lombok.AllArgsConstructor;
@@ -50,14 +53,13 @@ public class HoldingsDataProcessor extends AbstractDataProcessor<ExtendedHolding
   private final HoldingsNotesUpdater holdingsNotesUpdater;
   private final ElectronicAccessUpdaterFactory electronicAccessUpdaterFactory;
   private final ElectronicAccessReferenceService electronicAccessReferenceService;
-  private final FolioExecutionContext folioExecutionContext;
 
   private static final Pattern UUID_REGEX =
     Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$");
 
   @Override
-  public Validator<UpdateOptionType, Action> validator(ExtendedHoldingsRecord extendedHoldingsRecord) {
-    return (option, action) -> {
+  public Validator<UpdateOptionType, Action, BulkOperationRule> validator(ExtendedHoldingsRecord extendedHoldingsRecord) {
+    return (option, action, rule) -> {
       try {
         if ("MARC".equals(holdingsReferenceService.getSourceById(extendedHoldingsRecord.getEntity().getSourceId(), folioExecutionContext.getTenantId()).getName())) {
           throw new RuleValidationException("Holdings records that have source \"MARC\" cannot be changed");
@@ -74,7 +76,10 @@ public Validator<UpdateOptionType, Action> validator(ExtendedHoldingsRecord exte
     };
   }
 
-  public Updater<ExtendedHoldingsRecord> updater(UpdateOptionType option, Action action) {
+  public Updater<ExtendedHoldingsRecord> updater(UpdateOptionType option, Action action, ExtendedHoldingsRecord entity, BulkOperationRule rule) throws RuleValidationTenantsException {
+    if (ruleTenantsAreNotValid(rule, action, entity)) {
+      throw new RuleValidationTenantsException(String.format(RECORD_CANNOT_BE_UPDATED_ERROR_TEMPLATE, entity.getIdentifier(org.folio.bulkops.domain.dto.IdentifierType.ID), entity.getTenant(), option.getValue()));
+    }
     if (isElectronicAccessUpdate(option)) {
       return (Updater<ExtendedHoldingsRecord>) electronicAccessUpdaterFactory.updater(option, action);
     } else if (REPLACE_WITH == action.getType()) {
@@ -181,4 +186,11 @@ public boolean compare(ExtendedHoldingsRecord first, ExtendedHoldingsRecord seco
   public Class<ExtendedHoldingsRecord> getProcessedType() {
     return ExtendedHoldingsRecord.class;
   }
+
+  private boolean ruleTenantsAreNotValid(BulkOperationRule rule, Action action, ExtendedHoldingsRecord extendedHolding) {
+    var ruleTenants = rule.getRuleDetails().getTenants();
+    var actionTenants = action.getTenants();
+    return nonNull(ruleTenants) && !ruleTenants.isEmpty() && !ruleTenants.contains(extendedHolding.getTenant()) ||
+      nonNull(actionTenants) && !actionTenants.isEmpty() && !actionTenants.contains(extendedHolding.getTenant());
+  }
 }
diff --git a/src/main/java/org/folio/bulkops/processor/ItemDataProcessor.java b/src/main/java/org/folio/bulkops/processor/ItemDataProcessor.java
index fcfe4482..993fa624 100644
--- a/src/main/java/org/folio/bulkops/processor/ItemDataProcessor.java
+++ b/src/main/java/org/folio/bulkops/processor/ItemDataProcessor.java
@@ -2,6 +2,7 @@
 
 import static java.lang.String.format;
 import static java.util.Objects.isNull;
+import static java.util.Objects.nonNull;
 import static org.apache.commons.lang3.StringUtils.isEmpty;
 import static org.folio.bulkops.domain.dto.UpdateActionType.CLEAR_FIELD;
 import static org.folio.bulkops.domain.dto.UpdateActionType.REPLACE_WITH;
@@ -10,6 +11,7 @@
 import static org.folio.bulkops.domain.dto.UpdateOptionType.PERMANENT_LOAN_TYPE;
 import static org.folio.bulkops.domain.dto.UpdateOptionType.STATUS;
 import static org.folio.bulkops.domain.dto.UpdateOptionType.SUPPRESS_FROM_DISCOVERY;
+import static org.folio.bulkops.util.Constants.RECORD_CANNOT_BE_UPDATED_ERROR_TEMPLATE;
 
 import java.util.ArrayList;
 import java.util.Date;
@@ -22,11 +24,12 @@
 import org.folio.bulkops.domain.bean.ItemLocation;
 import org.folio.bulkops.domain.dto.Action;
 import org.folio.bulkops.domain.dto.UpdateOptionType;
+import org.folio.bulkops.domain.dto.BulkOperationRule;
 import org.folio.bulkops.exception.BulkOperationException;
 import org.folio.bulkops.exception.RuleValidationException;
+import org.folio.bulkops.exception.RuleValidationTenantsException;
 import org.folio.bulkops.service.HoldingsReferenceService;
 import org.folio.bulkops.service.ItemReferenceService;
-import org.folio.spring.FolioExecutionContext;
 import org.springframework.stereotype.Component;
 
 import lombok.AllArgsConstructor;
@@ -39,11 +42,10 @@ public class ItemDataProcessor extends AbstractDataProcessor<ExtendedItem> {
   private final HoldingsReferenceService holdingsReferenceService;
   private final ItemReferenceService itemReferenceService;
   private final ItemsNotesUpdater itemsNotesUpdater;
-  private final FolioExecutionContext folioExecutionContext;
 
   @Override
-  public Validator<UpdateOptionType, Action> validator(ExtendedItem extendedItem) {
-    return (option, action) -> {
+  public Validator<UpdateOptionType, Action, BulkOperationRule> validator(ExtendedItem extendedItem) {
+    return (option, action, rule) -> {
       if (CLEAR_FIELD == action.getType() && STATUS == option) {
         throw new RuleValidationException("Status field can not be cleared");
       } else if (CLEAR_FIELD == action.getType() && PERMANENT_LOAN_TYPE == option) {
@@ -66,7 +68,10 @@ public Validator<UpdateOptionType, Action> validator(ExtendedItem extendedItem)
   }
 
   @Override
-  public Updater<ExtendedItem> updater(UpdateOptionType option, Action action) {
+  public Updater<ExtendedItem> updater(UpdateOptionType option, Action action, ExtendedItem entity, BulkOperationRule rule) throws RuleValidationTenantsException {
+    if (ruleTenantsAreNotValid(rule, action, entity)) {
+      throw new RuleValidationTenantsException(String.format(RECORD_CANNOT_BE_UPDATED_ERROR_TEMPLATE, entity.getIdentifier(org.folio.bulkops.domain.dto.IdentifierType.ID), entity.getTenant(), option.getValue()));
+    }
     if (REPLACE_WITH == action.getType()) {
       return switch (option) {
         case PERMANENT_LOAN_TYPE ->
@@ -156,5 +161,12 @@ private ItemLocation getEffectiveLocation(Item item) {
       return isNull(item.getTemporaryLocation()) ? item.getPermanentLocation() : item.getTemporaryLocation();
     }
   }
+
+  private boolean ruleTenantsAreNotValid(BulkOperationRule rule, Action action, ExtendedItem extendedItem) {
+    var ruleTenants = rule.getRuleDetails().getTenants();
+    var actionTenants = action.getTenants();
+    return nonNull(ruleTenants) && !ruleTenants.isEmpty() && !ruleTenants.contains(extendedItem.getTenant()) ||
+      nonNull(actionTenants) && !actionTenants.isEmpty() && !actionTenants.contains(extendedItem.getTenant());
+  }
 }
 
diff --git a/src/main/java/org/folio/bulkops/processor/UserDataProcessor.java b/src/main/java/org/folio/bulkops/processor/UserDataProcessor.java
index 8018566f..0cf48f49 100644
--- a/src/main/java/org/folio/bulkops/processor/UserDataProcessor.java
+++ b/src/main/java/org/folio/bulkops/processor/UserDataProcessor.java
@@ -16,6 +16,7 @@
 import org.folio.bulkops.domain.bean.User;
 import org.folio.bulkops.domain.dto.Action;
 import org.folio.bulkops.domain.dto.UpdateOptionType;
+import org.folio.bulkops.domain.dto.BulkOperationRule;
 import org.folio.bulkops.exception.BulkOperationException;
 import org.folio.bulkops.exception.RuleValidationException;
 import org.folio.bulkops.service.UserReferenceService;
@@ -33,8 +34,8 @@ public class UserDataProcessor extends AbstractDataProcessor<User> {
   private final UserReferenceService userReferenceService;
 
   @Override
-  public Validator<UpdateOptionType, Action> validator(User entity) {
-    return (option, action) -> {
+  public Validator<UpdateOptionType, Action, BulkOperationRule> validator(User entity) {
+    return (option, action, rule) -> {
       if (EXPIRATION_DATE == option) {
         if (action.getType() != REPLACE_WITH) {
           throw new RuleValidationException(
@@ -63,7 +64,7 @@ public Validator<UpdateOptionType, Action> validator(User entity) {
   }
 
   @Override
-  public Updater<User> updater(UpdateOptionType option, Action action) {
+  public Updater<User> updater(UpdateOptionType option, Action action, User entity, BulkOperationRule rule) {
     return switch (option) {
       case PATRON_GROUP -> user -> user.setPatronGroup(action.getUpdated());
       case EXPIRATION_DATE -> user -> {
diff --git a/src/main/java/org/folio/bulkops/processor/Validator.java b/src/main/java/org/folio/bulkops/processor/Validator.java
index 29974660..4ab709f8 100644
--- a/src/main/java/org/folio/bulkops/processor/Validator.java
+++ b/src/main/java/org/folio/bulkops/processor/Validator.java
@@ -1,8 +1,9 @@
 package org.folio.bulkops.processor;
 
 import org.folio.bulkops.exception.RuleValidationException;
+import org.folio.bulkops.exception.RuleValidationTenantsException;
 
 @FunctionalInterface
-public interface Validator<T, U> {
-    void validate(T t, U u) throws RuleValidationException;
+public interface Validator<T, U, V> {
+    void validate(T t, U u, V v) throws RuleValidationException, RuleValidationTenantsException;
 }
diff --git a/src/main/java/org/folio/bulkops/service/ConsortiaService.java b/src/main/java/org/folio/bulkops/service/ConsortiaService.java
index ee2d15cd..eb948154 100644
--- a/src/main/java/org/folio/bulkops/service/ConsortiaService.java
+++ b/src/main/java/org/folio/bulkops/service/ConsortiaService.java
@@ -29,10 +29,10 @@ public String getCentralTenantId(String currentTenantId) {
     var userTenantCollection = consortiaClient.getUserTenantCollection();
     var userTenants = userTenantCollection.getUserTenants();
     if (!userTenants.isEmpty()) {
-      log.debug("userTenants: {}", userTenants);
+      log.info("userTenants: {}", userTenants);
       return userTenants.get(0).getCentralTenantId();
     }
-    log.debug("No central tenant found for {}", currentTenantId);
+    log.info("No central tenant found for {}", currentTenantId);
     return StringUtils.EMPTY;
   }
 
diff --git a/src/main/java/org/folio/bulkops/service/RuleService.java b/src/main/java/org/folio/bulkops/service/RuleService.java
index e573141b..694b7413 100644
--- a/src/main/java/org/folio/bulkops/service/RuleService.java
+++ b/src/main/java/org/folio/bulkops/service/RuleService.java
@@ -47,6 +47,7 @@ public BulkOperationRuleCollection saveRules(BulkOperationRuleCollection ruleCol
           .initialValue(action.getInitial())
           .updatedValue(action.getUpdated())
           .parameters(action.getParameters())
+          .tenants(action.getTenants())
           .build()));
     });
     return ruleCollection;
@@ -72,7 +73,8 @@ private BulkOperationRule mapBulkOperationRuleToDto(org.folio.bulkops.domain.ent
             .type(details.getUpdateAction())
             .initial(details.getInitialValue())
             .updated(details.getUpdatedValue())
-            .parameters(details.getParameters()))
+            .parameters(details.getParameters())
+            .tenants(details.getTenants()))
           .toList()));
   }
 
diff --git a/src/main/java/org/folio/bulkops/util/Constants.java b/src/main/java/org/folio/bulkops/util/Constants.java
index 5bb97161..c0d4d3bc 100644
--- a/src/main/java/org/folio/bulkops/util/Constants.java
+++ b/src/main/java/org/folio/bulkops/util/Constants.java
@@ -62,6 +62,7 @@ public class Constants {
   public static final String MSG_ERROR_OPTIMISTIC_LOCKING_DEFAULT = "The record cannot be saved because it is not the most recent version.";
 
   public static final String CSV_MSG_ERROR_TEMPLATE_OPTIMISTIC_LOCKING = "The record cannot be saved because it is not the most recent version. Stored version is %s, bulk edit version is %s.";
+  public static final String RECORD_CANNOT_BE_UPDATED_ERROR_TEMPLATE = "%s cannot be updated because the record is associated with %s and %s is not associated with this tenant.";
   public static final String ITEM_TYPE = "ITEM";
   public static final String HOLDING_TYPE = "HOLDINGS_RECORD";
   public static final Set<String> SPLIT_NOTE_ENTITIES = Set.of(ITEM_TYPE, HOLDING_TYPE);
diff --git a/src/main/resources/db/changelog/changelog-master.xml b/src/main/resources/db/changelog/changelog-master.xml
index ff8db8c2..17e2b8e8 100644
--- a/src/main/resources/db/changelog/changelog-master.xml
+++ b/src/main/resources/db/changelog/changelog-master.xml
@@ -30,4 +30,5 @@
   <include file="changes/14-06-2024_add_marc_links_to_bulk_operation_table.xml" relativeToChangelogFile="true"/>
   <include file="changes/18-06-2024_updates_for_editing_marc.xml" relativeToChangelogFile="true"/>
   <include file="changes/29-08-2024_add_used_tenants.xml" relativeToChangelogFile="true"/>
+  <include file="changes/20-09-2024_add_tenants_to_rules.xml" relativeToChangelogFile="true"/>
 </databaseChangeLog>
diff --git a/src/main/resources/db/changelog/changes/20-09-2024_add_tenants_to_rules.sql b/src/main/resources/db/changelog/changes/20-09-2024_add_tenants_to_rules.sql
new file mode 100644
index 00000000..ed35cc4d
--- /dev/null
+++ b/src/main/resources/db/changelog/changes/20-09-2024_add_tenants_to_rules.sql
@@ -0,0 +1,2 @@
+ALTER TABLE bulk_operation_rule_details
+ADD COLUMN IF NOT EXISTS tenants TEXT[];
diff --git a/src/main/resources/db/changelog/changes/20-09-2024_add_tenants_to_rules.xml b/src/main/resources/db/changelog/changes/20-09-2024_add_tenants_to_rules.xml
new file mode 100644
index 00000000..94a91cbe
--- /dev/null
+++ b/src/main/resources/db/changelog/changes/20-09-2024_add_tenants_to_rules.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<databaseChangeLog
+  xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
+                      http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
+
+
+  <changeSet id="20-09-2024_add_tenants_to_rules.sql" author="firebird">
+    <sqlFile path="20-09-2024_add_tenants_to_rules.sql" relativeToChangelogFile="true" />
+  </changeSet>
+
+</databaseChangeLog>
diff --git a/src/main/resources/swagger.api/schemas/action.json b/src/main/resources/swagger.api/schemas/action.json
index a6922eaf..8db7332f 100644
--- a/src/main/resources/swagger.api/schemas/action.json
+++ b/src/main/resources/swagger.api/schemas/action.json
@@ -21,6 +21,12 @@
         "items": {
           "$ref": "action_parameter.json#/Parameter"
         }
+      },
+      "tenants": {
+        "type": "array",
+        "items": {
+          "type": "string"
+        }
       }
     },
     "required": [
diff --git a/src/main/resources/swagger.api/schemas/bulk_operation_rule.json b/src/main/resources/swagger.api/schemas/bulk_operation_rule.json
index 6aace308..5832c466 100644
--- a/src/main/resources/swagger.api/schemas/bulk_operation_rule.json
+++ b/src/main/resources/swagger.api/schemas/bulk_operation_rule.json
@@ -22,6 +22,12 @@
             "description": "Option to change",
             "$ref": "update_option_type.json#/UpdateOptionType"
           },
+          "tenants": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
           "actions": {
             "type": "array",
             "items": {
diff --git a/src/test/java/org/folio/bulkops/BaseTest.java b/src/test/java/org/folio/bulkops/BaseTest.java
index 63e75728..40b4a780 100644
--- a/src/test/java/org/folio/bulkops/BaseTest.java
+++ b/src/test/java/org/folio/bulkops/BaseTest.java
@@ -298,6 +298,14 @@ public static BulkOperationRule rule(UpdateOptionType option, UpdateActionType a
     return rule(option, action, null, updated);
   }
 
+  public static BulkOperationRule rule(UpdateOptionType option, UpdateActionType action, String updated,
+                                       List<String> actionsTenants, List<String> ruleTenants) {
+    var rule = rule(option, action, null, updated);
+    rule.getRuleDetails().setTenants(ruleTenants);
+    rule.getRuleDetails().getActions().get(0).setTenants(actionsTenants);
+    return rule;
+  }
+
   public BulkOperation buildBulkOperation(String fileName, org.folio.bulkops.domain.dto.EntityType entityType, org.folio.bulkops.domain.dto.BulkOperationStep step) {
     return switch (step) {
       case UPLOAD -> BulkOperation.builder()
diff --git a/src/test/java/org/folio/bulkops/processor/FolioInstanceDataProcessorTest.java b/src/test/java/org/folio/bulkops/processor/FolioInstanceDataProcessorTest.java
index e8644b41..a1944495 100644
--- a/src/test/java/org/folio/bulkops/processor/FolioInstanceDataProcessorTest.java
+++ b/src/test/java/org/folio/bulkops/processor/FolioInstanceDataProcessorTest.java
@@ -385,7 +385,7 @@ void shouldNotUpdateNotesIfSourceIsNotFolio(UpdateActionType actionType) {
     var extendedInstance = ExtendedInstance.builder().entity(instance).tenantId("tenantId").build();
     var validator = ((FolioInstanceDataProcessor) processor).validator(extendedInstance);
 
-    assertThrows(RuleValidationException.class, () -> validator.validate(INSTANCE_NOTE, new Action().type(actionType)));
+    assertThrows(RuleValidationException.class, () -> validator.validate(INSTANCE_NOTE, new Action().type(actionType), new BulkOperationRule()));
   }
 
   @Test
@@ -398,6 +398,6 @@ void shouldNotChangeTypeForAdministrativeNotesIfSourceIsNotFolio() {
     var extendedInstance = ExtendedInstance.builder().entity(instance).tenantId("tenantId").build();
     var validator = ((FolioInstanceDataProcessor) processor).validator(extendedInstance);
 
-    assertThrows(RuleValidationException.class, () -> validator.validate(ADMINISTRATIVE_NOTE, new Action().type(CHANGE_TYPE)));
+    assertThrows(RuleValidationException.class, () -> validator.validate(ADMINISTRATIVE_NOTE, new Action().type(CHANGE_TYPE), new BulkOperationRule()));
   }
 }
diff --git a/src/test/java/org/folio/bulkops/processor/HoldingsDataProcessorTest.java b/src/test/java/org/folio/bulkops/processor/HoldingsDataProcessorTest.java
index 921e8a5b..97eb38ce 100644
--- a/src/test/java/org/folio/bulkops/processor/HoldingsDataProcessorTest.java
+++ b/src/test/java/org/folio/bulkops/processor/HoldingsDataProcessorTest.java
@@ -28,6 +28,10 @@
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 import java.util.ArrayList;
@@ -43,21 +47,28 @@
 import org.folio.bulkops.domain.bean.HoldingsRecordsSource;
 import org.folio.bulkops.domain.bean.ItemLocation;
 import org.folio.bulkops.domain.dto.Action;
+import org.folio.bulkops.domain.dto.BulkOperationRuleRuleDetails;
+import org.folio.bulkops.domain.dto.BulkOperationRule;
 import org.folio.bulkops.domain.dto.Parameter;
 import org.folio.bulkops.domain.dto.UpdateOptionType;
 import org.folio.bulkops.exception.NotFoundException;
 import org.folio.bulkops.repository.BulkOperationExecutionContentRepository;
+import org.folio.bulkops.service.ConsortiaService;
 import org.folio.bulkops.service.ElectronicAccessService;
 import org.folio.bulkops.service.ErrorService;
+import org.folio.bulkops.util.FolioExecutionContextUtil;
+import org.folio.spring.FolioExecutionContext;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mockito;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.mock.mockito.MockBean;
 
 import feign.FeignException;
+import org.springframework.boot.test.mock.mockito.SpyBean;
 
 class HoldingsDataProcessorTest extends BaseTest {
 
@@ -70,6 +81,10 @@ class HoldingsDataProcessorTest extends BaseTest {
   ErrorService errorService;
   @MockBean
   ElectronicAccessService electronicAccessService;
+  @MockBean
+  private ConsortiaService consortiaService;
+  @SpyBean
+  private FolioExecutionContext folioExecutionContext;
 
   private DataProcessor<ExtendedHoldingsRecord> processor;
 
@@ -276,15 +291,15 @@ void testUpdaterForSuppressFromDiscoveryOption() {
       .withDiscoverySuppress(false);
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holdingsRecord).tenantId("tenant").build();
 
-    var processor = new HoldingsDataProcessor(null, null, null, null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, null, null, null);
 
-    processor.updater(SUPPRESS_FROM_DISCOVERY, new Action().type(SET_TO_TRUE)).apply(extendedHoldingsRecord);
+    processor.updater(SUPPRESS_FROM_DISCOVERY, new Action().type(SET_TO_TRUE), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertTrue(holdingsRecord.getDiscoverySuppress());
-    processor.updater(SUPPRESS_FROM_DISCOVERY, new Action().type(SET_TO_FALSE)).apply(extendedHoldingsRecord);
+    processor.updater(SUPPRESS_FROM_DISCOVERY, new Action().type(SET_TO_FALSE), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertFalse(holdingsRecord.getDiscoverySuppress());
-    processor.updater(SUPPRESS_FROM_DISCOVERY, new Action().type(SET_TO_TRUE_INCLUDING_ITEMS)).apply(extendedHoldingsRecord);
+    processor.updater(SUPPRESS_FROM_DISCOVERY, new Action().type(SET_TO_TRUE_INCLUDING_ITEMS), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertTrue(holdingsRecord.getDiscoverySuppress());
-    processor.updater(SUPPRESS_FROM_DISCOVERY, new Action().type(SET_TO_FALSE_INCLUDING_ITEMS)).apply(extendedHoldingsRecord);
+    processor.updater(SUPPRESS_FROM_DISCOVERY, new Action().type(SET_TO_FALSE_INCLUDING_ITEMS), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertFalse(holdingsRecord.getDiscoverySuppress());
   }
 
@@ -299,9 +314,9 @@ void testUpdateMarkAsStaffOnlyForHoldingsNotes() {
     var parameter = new Parameter();
     parameter.setKey(HOLDINGS_NOTE_TYPE_ID_KEY);
     parameter.setValue("typeId");
-    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null);
 
-    processor.updater(HOLDINGS_NOTE, new Action().type(MARK_AS_STAFF_ONLY).parameters(List.of(parameter))).apply(extendedHoldingsRecord);
+    processor.updater(HOLDINGS_NOTE, new Action().type(MARK_AS_STAFF_ONLY).parameters(List.of(parameter)), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
 
     assertTrue(holding.getNotes().get(0).getStaffOnly());
   }
@@ -317,9 +332,9 @@ void testUpdateRemoveMarkAsStaffOnlyForHoldingsNotes() {
     var parameter = new Parameter();
     parameter.setKey(HOLDINGS_NOTE_TYPE_ID_KEY);
     parameter.setValue("typeId");
-    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null);
 
-    processor.updater(HOLDINGS_NOTE, new Action().type(REMOVE_MARK_AS_STAFF_ONLY).parameters(List.of(parameter))).apply(extendedHoldingsRecord);
+    processor.updater(HOLDINGS_NOTE, new Action().type(REMOVE_MARK_AS_STAFF_ONLY).parameters(List.of(parameter)), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
 
     assertFalse(holding.getNotes().get(0).getStaffOnly());
   }
@@ -331,9 +346,9 @@ void testRemoveAdministrativeNotes() {
     var holding =  new HoldingsRecord().withAdministrativeNotes(List.of(administrativeNote));
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holding).tenantId("tenant").build();
 
-    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null);
 
-    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(REMOVE_ALL)).apply(extendedHoldingsRecord);
+    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(REMOVE_ALL), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertTrue(holding.getAdministrativeNotes().isEmpty());
   }
 
@@ -347,9 +362,9 @@ void testRemoveHoldingsNotes() {
     var parameter = new Parameter();
     parameter.setKey(HOLDINGS_NOTE_TYPE_ID_KEY);
     parameter.setValue("typeId1");
-    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null);
 
-    processor.updater(HOLDINGS_NOTE, new Action().type(REMOVE_ALL).parameters(List.of(parameter))).apply(extendedHoldingsRecord);
+    processor.updater(HOLDINGS_NOTE, new Action().type(REMOVE_ALL).parameters(List.of(parameter)), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertEquals(1, holding.getNotes().size());
     assertEquals("typeId2", holding.getNotes().get(0).getHoldingsNoteTypeId());
   }
@@ -361,13 +376,13 @@ void testAddAdministrativeNotes() {
     var administrativeNote2 = "administrative note 2";
     var holding = new HoldingsRecord();
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holding).tenantId("tenant").build();
-    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null);
 
-    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(ADD_TO_EXISTING).updated(administrativeNote1)).apply(extendedHoldingsRecord);
+    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(ADD_TO_EXISTING).updated(administrativeNote1), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertEquals(1, holding.getAdministrativeNotes().size());
     assertEquals(administrativeNote1, holding.getAdministrativeNotes().get(0));
 
-    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(ADD_TO_EXISTING).updated(administrativeNote2)).apply(extendedHoldingsRecord);
+    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(ADD_TO_EXISTING).updated(administrativeNote2), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertEquals(2, holding.getAdministrativeNotes().size());
   }
 
@@ -382,9 +397,9 @@ void testAddHoldingsNotes() {
     parameter.setKey(HOLDINGS_NOTE_TYPE_ID_KEY);
     parameter.setValue("typeId1");
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holding).tenantId("tenant").build();
-    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null);
 
-    processor.updater(HOLDINGS_NOTE, new Action().type(ADD_TO_EXISTING).parameters(List.of(parameter)).updated(note1)).apply(extendedHoldingsRecord);
+    processor.updater(HOLDINGS_NOTE, new Action().type(ADD_TO_EXISTING).parameters(List.of(parameter)).updated(note1), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
 
     assertEquals(1, holding.getNotes().size());
     assertEquals("typeId1", holding.getNotes().get(0).getHoldingsNoteTypeId());
@@ -392,7 +407,7 @@ void testAddHoldingsNotes() {
     assertEquals(false, holding.getNotes().get(0).getStaffOnly());
 
     parameter.setValue("typeId2");
-    processor.updater(HOLDINGS_NOTE, new Action().type(ADD_TO_EXISTING).parameters(List.of(parameter)).updated(note2)).apply(extendedHoldingsRecord);
+    processor.updater(HOLDINGS_NOTE, new Action().type(ADD_TO_EXISTING).parameters(List.of(parameter)).updated(note2), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
 
     assertEquals(2, holding.getNotes().size());
     assertEquals("typeId2", holding.getNotes().get(1).getHoldingsNoteTypeId());
@@ -401,7 +416,7 @@ void testAddHoldingsNotes() {
 
     parameter.setValue("typeId3");
     List<Parameter> params = List.of(new Parameter().key(STAFF_ONLY_NOTE_PARAMETER_KEY).value("true"), parameter);
-    processor.updater(HOLDINGS_NOTE, new Action().type(ADD_TO_EXISTING).parameters(params).updated(note3)).apply(extendedHoldingsRecord);
+    processor.updater(HOLDINGS_NOTE, new Action().type(ADD_TO_EXISTING).parameters(params).updated(note3), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertEquals(3, holding.getNotes().size());
     assertEquals("typeId3", holding.getNotes().get(2).getHoldingsNoteTypeId());
     assertEquals(note3, holding.getNotes().get(2).getNote());
@@ -415,12 +430,12 @@ void testFindAndRemoveForAdministrativeNotes() {
     var administrativeNote2 = "administrative note 2";
     var holding = new HoldingsRecord().withAdministrativeNotes(new ArrayList<>(List.of(administrativeNote1, administrativeNote2)));
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holding).tenantId("tenant").build();
-    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null);
 
-    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("administrative note")).apply(extendedHoldingsRecord);
+    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("administrative note"), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertEquals(2, holding.getAdministrativeNotes().size());
 
-    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial(administrativeNote2)).apply(extendedHoldingsRecord);
+    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial(administrativeNote2), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertEquals(1, holding.getAdministrativeNotes().size());
     assertEquals(administrativeNote1, holding.getAdministrativeNotes().get(0));
   }
@@ -436,16 +451,16 @@ void testFindAndRemoveHoldingsNotes() {
     parameter.setValue("typeId1");
     var holding = new HoldingsRecord().withNotes(List.of(note1, note2, note3));
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holding).tenantId("tenant").build();
-    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null);
 
     processor.updater(HOLDINGS_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("note")
-      .parameters(List.of(parameter))).apply(extendedHoldingsRecord);
+      .parameters(List.of(parameter)), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertEquals(3, holding.getNotes().size());
     processor.updater(HOLDINGS_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("note1")
-      .parameters(List.of(parameter))).apply(extendedHoldingsRecord);
+      .parameters(List.of(parameter)), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertEquals(2, holding.getNotes().size());
     processor.updater(HOLDINGS_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("note2")
-      .parameters(List.of(parameter))).apply(extendedHoldingsRecord);
+      .parameters(List.of(parameter)), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertEquals(1, holding.getNotes().size());
   }
 
@@ -457,10 +472,10 @@ void testFindAndReplaceForAdministrativeNotes() {
     var administrativeNote3 = "administrative note 3";
     var holding = new HoldingsRecord().withAdministrativeNotes(new ArrayList<>(List.of(administrativeNote1, administrativeNote2)));
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holding).tenantId("tenant").build();
-    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null);
 
     processor.updater(ADMINISTRATIVE_NOTE, new Action().type(FIND_AND_REPLACE)
-      .initial(administrativeNote1).updated(administrativeNote3)).apply(extendedHoldingsRecord);
+      .initial(administrativeNote1).updated(administrativeNote3), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertEquals(2, holding.getAdministrativeNotes().size());
     assertEquals(administrativeNote3, holding.getAdministrativeNotes().get(0));
     assertEquals(administrativeNote2, holding.getAdministrativeNotes().get(1));
@@ -476,10 +491,10 @@ void testFindAndReplaceForHoldingNotes() {
     parameter.setValue("typeId1");
     var holding = new HoldingsRecord().withNotes(List.of(note1, note2));
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holding).tenantId("tenant").build();
-    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null);
 
     processor.updater(HOLDINGS_NOTE, new Action().type(FIND_AND_REPLACE).parameters(List.of(parameter))
-      .initial("note1").updated("note3")).apply(extendedHoldingsRecord);
+      .initial("note1").updated("note3"), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
 
     assertEquals("note3", holding.getNotes().get(0).getNote());
     assertEquals("note1", holding.getNotes().get(1).getNote());
@@ -491,10 +506,10 @@ void testChangeTypeForAdministrativeNotes() {
     var administrativeNote = "note";
     var holding = new HoldingsRecord().withAdministrativeNotes(new ArrayList<>(List.of(administrativeNote)));
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holding).tenantId("tenant").build();
-    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null);
 
     processor.updater(ADMINISTRATIVE_NOTE, new Action().type(CHANGE_TYPE)
-      .updated("typeId")).apply(extendedHoldingsRecord);
+      .updated("typeId"), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertEquals(0, holding.getAdministrativeNotes().size());
     assertEquals("note", holding.getNotes().get(0).getNote());
     assertEquals("typeId", holding.getNotes().get(0).getHoldingsNoteTypeId());
@@ -510,9 +525,9 @@ void testChangeNoteTypeForHoldingsNotes() {
     parameter.setValue("typeId1");
     var holding = new HoldingsRecord().withNotes(List.of(note1, note2));
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holding).tenantId("tenant").build();
-    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null);
 
-    processor.updater(HOLDINGS_NOTE, new Action().type(CHANGE_TYPE).updated(ADMINISTRATIVE_NOTE.getValue()).parameters(List.of(parameter))).apply(extendedHoldingsRecord);
+    processor.updater(HOLDINGS_NOTE, new Action().type(CHANGE_TYPE).updated(ADMINISTRATIVE_NOTE.getValue()).parameters(List.of(parameter)), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
 
     assertEquals(1, holding.getAdministrativeNotes().size());
     assertEquals("note1", holding.getAdministrativeNotes().get(0));
@@ -522,7 +537,7 @@ void testChangeNoteTypeForHoldingsNotes() {
     holding.setAdministrativeNotes(null);
     holding.setNotes(List.of(note1, note2));
 
-    processor.updater(HOLDINGS_NOTE, new Action().type(CHANGE_TYPE).updated("typeId3").parameters(List.of(parameter))).apply(extendedHoldingsRecord);
+    processor.updater(HOLDINGS_NOTE, new Action().type(CHANGE_TYPE).updated("typeId3").parameters(List.of(parameter)), extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
     assertEquals(2, holding.getNotes().size());
     assertEquals("note1", holding.getNotes().get(0).getNote());
     assertEquals("typeId3", holding.getNotes().get(0).getHoldingsNoteTypeId());
@@ -533,7 +548,7 @@ void testChangeNoteTypeForHoldingsNotes() {
   @Test
   @SneakyThrows
   void testClone() {
-    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, new HoldingsNotesUpdater(new AdministrativeNotesUpdater()), null, null);
     var administrativeNotes = new ArrayList<String>();
     administrativeNotes.add("note1");
     var holding1 = new HoldingsRecord().withId("id")
@@ -574,11 +589,11 @@ void testClone() {
   void shouldClearElectronicAccessFields(UpdateOptionType option) {
     var holdingsRecord = buildHoldingsWithElectronicAccess();
 
-    var processor = new HoldingsDataProcessor(null, null, null, new ElectronicAccessUpdaterFactory(), null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, null, new ElectronicAccessUpdaterFactory(), null);
     var action = new Action().type(CLEAR_FIELD);
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holdingsRecord).tenantId("tenant").build();
 
-    processor.updater(option, action).apply(extendedHoldingsRecord);
+    processor.updater(option, action, extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
 
     var electronicAccess = holdingsRecord.getElectronicAccess().get(0);
     switch (option) {
@@ -602,10 +617,10 @@ void shouldClearElectronicAccessFields(UpdateOptionType option) {
   void shouldFindAndClearExactlyMatchedElectronicAccessFields(UpdateOptionType option, String value) {
     var holdingsRecord = buildHoldingsWithElectronicAccess();
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holdingsRecord).tenantId("tenant").build();
-    var processor = new HoldingsDataProcessor(null, null, null, new ElectronicAccessUpdaterFactory(), null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, null, new ElectronicAccessUpdaterFactory(), null);
     var action = new Action().type(FIND_AND_REMOVE_THESE).initial(value);
 
-    processor.updater(option, action).apply(extendedHoldingsRecord);
+    processor.updater(option, action, extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
 
     var modified = holdingsRecord.getElectronicAccess().get(0);
     var unmodified = holdingsRecord.getElectronicAccess().get(1);
@@ -647,10 +662,10 @@ void shouldFindAndClearExactlyMatchedElectronicAccessFields(UpdateOptionType opt
   void shouldReplaceElectronicAccessFields(UpdateOptionType option, String newValue) {
     var holdingsRecord = buildHoldingsWithElectronicAccess();
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holdingsRecord).tenantId("tenant").build();
-    var processor = new HoldingsDataProcessor(null, null, null, new ElectronicAccessUpdaterFactory(), null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, null, new ElectronicAccessUpdaterFactory(), null);
     var action = new Action().type(REPLACE_WITH).updated(newValue);
 
-    processor.updater(option, action).apply(extendedHoldingsRecord);
+    processor.updater(option, action, extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
 
     var electronicAccess = holdingsRecord.getElectronicAccess().get(0);
     switch (option) {
@@ -674,10 +689,10 @@ void shouldReplaceElectronicAccessFields(UpdateOptionType option, String newValu
   void shouldFindAndReplaceExactlyMatchedElectronicAccessFields(UpdateOptionType option, String initial, String updated) {
     var holdingsRecord = buildHoldingsWithElectronicAccess();
     var extendedHoldingsRecord = ExtendedHoldingsRecord.builder().entity(holdingsRecord).tenantId("tenant").build();
-    var processor = new HoldingsDataProcessor(null, null, null, new ElectronicAccessUpdaterFactory(), null, folioExecutionContext);
+    var processor = new HoldingsDataProcessor(null, null, null, new ElectronicAccessUpdaterFactory(), null);
     var action = new Action().type(FIND_AND_REPLACE).initial(initial).updated(updated);
 
-    processor.updater(option, action).apply(extendedHoldingsRecord);
+    processor.updater(option, action, extendedHoldingsRecord, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedHoldingsRecord);
 
     var modified = holdingsRecord.getElectronicAccess().get(0);
     var unmodified = holdingsRecord.getElectronicAccess().get(1);
@@ -707,6 +722,80 @@ void shouldFindAndReplaceExactlyMatchedElectronicAccessFields(UpdateOptionType o
     }
   }
 
+  @Test
+  void testShouldNotUpdateHoldingWithPermanentLocation_whenLocationFromOtherTenantThanActionTenants() {
+    when(folioExecutionContext.getTenantId()).thenReturn("memberB");
+    when(consortiaService.getCentralTenantId("memberB")).thenReturn("central");
+
+    try (var ignored = Mockito.mockStatic(FolioExecutionContextUtil.class)) {
+      when(FolioExecutionContextUtil.prepareContextForTenant(any(), any(), any())).thenReturn(folioExecutionContext);
+
+      var permLocationFromMemberB = UUID.randomUUID().toString();
+      var actionTenants = List.of("memberB");
+      var holdId = UUID.randomUUID().toString();
+      var initPermLocation = UUID.randomUUID().toString();
+      var extendedHolding = ExtendedHoldingsRecord.builder().entity(new HoldingsRecord().withId(holdId).withPermanentLocationId(initPermLocation)).tenantId("memberA").build();
+
+      var rules = rules(rule(PERMANENT_LOCATION, REPLACE_WITH, permLocationFromMemberB, actionTenants, List.of()));
+      var operationId = rules.getBulkOperationRules().get(0).getBulkOperationId();
+
+      var result = processor.process(IDENTIFIER, extendedHolding, rules);
+
+      assertNotNull(result);
+      assertEquals(initPermLocation, result.getUpdated().getEntity().getPermanentLocationId());
+
+      verify(errorService, times(1)).saveError(operationId, IDENTIFIER, String.format("%s cannot be updated because the record is associated with %s and %s is not associated with this tenant.",
+        holdId, "memberA", "PERMANENT_LOCATION").trim());
+    }
+  }
+
+  @Test
+  void testShouldNotUpdateHoldingWithPermanentLocation_whenLocationFromOtherTenantThanRuleTenants() {
+    when(folioExecutionContext.getTenantId()).thenReturn("memberB");
+    when(consortiaService.getCentralTenantId("memberB")).thenReturn("central");
+
+    try (var ignored = Mockito.mockStatic(FolioExecutionContextUtil.class)) {
+      when(FolioExecutionContextUtil.prepareContextForTenant(any(), any(), any())).thenReturn(folioExecutionContext);
+
+      var adminNoteFromMemberB = UUID.randomUUID().toString();
+      var ruleTenants = List.of("memberB");
+      var holdId = UUID.randomUUID().toString();
+      var initPermLocation = UUID.randomUUID().toString();
+      var extendedHolding = ExtendedHoldingsRecord.builder().entity(new HoldingsRecord().withId(holdId).withPermanentLocationId(initPermLocation)).tenantId("memberA").build();
+
+      var rules = rules(rule(PERMANENT_LOCATION, REPLACE_WITH, adminNoteFromMemberB, List.of(), ruleTenants));
+      var operationId = rules.getBulkOperationRules().get(0).getBulkOperationId();
+
+      var result = processor.process(IDENTIFIER, extendedHolding, rules);
+
+      assertNotNull(result);
+      assertEquals(initPermLocation, result.getUpdated().getEntity().getPermanentLocationId());
+
+      verify(errorService, times(1)).saveError(operationId, IDENTIFIER, String.format("%s cannot be updated because the record is associated with %s and %s is not associated with this tenant.",
+        holdId, "memberA", "PERMANENT_LOCATION").trim());
+    }
+  }
+
+  @Test
+  void testShouldUpdateHoldingWithLoanType_whenLoanTypeFromTenantAmongRuleTenants() {
+
+    var locationIdFromMemberB = UUID.randomUUID().toString();
+
+    var ruleTenants = List.of("memberB", "memberA");
+    var holdId = UUID.randomUUID().toString();
+    var initPermLocation = UUID.randomUUID().toString();
+    var extendedHold = ExtendedHoldingsRecord.builder().entity(new HoldingsRecord().withId(holdId).withPermanentLocationId(initPermLocation)).tenantId("memberA").build();
+
+    var rules = rules(rule(PERMANENT_LOCATION, REPLACE_WITH, locationIdFromMemberB, List.of(), ruleTenants));
+
+    var result = processor.process(IDENTIFIER, extendedHold, rules);
+
+    assertNotNull(result);
+    assertEquals(locationIdFromMemberB, result.getUpdated().getEntity().getPermanentLocationId());
+
+    verifyNoInteractions(errorService);
+  }
+
   private HoldingsRecord buildHoldingsWithElectronicAccess() {
     return HoldingsRecord.builder()
       .electronicAccess(List.of(
diff --git a/src/test/java/org/folio/bulkops/processor/ItemDataProcessorTest.java b/src/test/java/org/folio/bulkops/processor/ItemDataProcessorTest.java
index 0e0c1911..7887af8f 100644
--- a/src/test/java/org/folio/bulkops/processor/ItemDataProcessorTest.java
+++ b/src/test/java/org/folio/bulkops/processor/ItemDataProcessorTest.java
@@ -34,6 +34,10 @@
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 import java.util.ArrayList;
@@ -55,20 +59,27 @@
 import org.folio.bulkops.domain.dto.Parameter;
 import org.folio.bulkops.domain.dto.UpdateActionType;
 import org.folio.bulkops.domain.dto.UpdateOptionType;
+import org.folio.bulkops.domain.dto.BulkOperationRuleRuleDetails;
+import org.folio.bulkops.domain.dto.BulkOperationRule;
 import org.folio.bulkops.repository.BulkOperationExecutionContentRepository;
+import org.folio.bulkops.service.ConsortiaService;
 import org.folio.bulkops.service.ErrorService;
 import org.folio.bulkops.service.HoldingsReferenceService;
 import org.folio.bulkops.service.ItemReferenceService;
+import org.folio.bulkops.util.FolioExecutionContextUtil;
+import org.folio.spring.FolioExecutionContext;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
 import org.junit.jupiter.params.provider.EnumSource;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mockito;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.mock.mockito.MockBean;
 
 import lombok.SneakyThrows;
+import org.springframework.boot.test.mock.mockito.SpyBean;
 
 class ItemDataProcessorTest extends BaseTest {
 
@@ -80,6 +91,10 @@ class ItemDataProcessorTest extends BaseTest {
   private HoldingsReferenceService holdingsReferenceService;
   @MockBean
   private ItemReferenceService itemReferenceService;
+  @SpyBean
+  private FolioExecutionContext folioExecutionContext;
+  @MockBean
+  private ConsortiaService consortiaService;
 
   private DataProcessor<ExtendedItem> processor;
 
@@ -325,9 +340,9 @@ void testUpdateMarkAsStaffOnlyForItemNotes() {
     var parameter = new Parameter();
     parameter.setKey(ITEM_NOTE_TYPE_ID_KEY);
     parameter.setValue("typeId");
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(ITEM_NOTE, new Action().type(MARK_AS_STAFF_ONLY).parameters(List.of(parameter))).apply(extendedItem);
+    processor.updater(ITEM_NOTE, new Action().type(MARK_AS_STAFF_ONLY).parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
 
     assertTrue(item.getNotes().get(0).getStaffOnly());
   }
@@ -341,9 +356,9 @@ void testUpdateRemoveMarkAsStaffOnlyForItemNotes() {
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
     parameter.setKey(ITEM_NOTE_TYPE_ID_KEY);
     parameter.setValue("typeId");
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(ITEM_NOTE, new Action().type(REMOVE_MARK_AS_STAFF_ONLY).parameters(List.of(parameter))).apply(extendedItem);
+    processor.updater(ITEM_NOTE, new Action().type(REMOVE_MARK_AS_STAFF_ONLY).parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
 
     assertFalse(item.getNotes().get(0).getStaffOnly());
   }
@@ -357,15 +372,15 @@ void testUpdateMarkAsStaffOnlyForCirculationNotes() {
     var parameter = new Parameter();
     parameter.setKey(ITEM_NOTE_TYPE_ID_KEY);
     parameter.setValue("typeId");
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(CHECK_IN_NOTE, new Action().type(MARK_AS_STAFF_ONLY).parameters(List.of(parameter))).apply(extendedItem);
+    processor.updater(CHECK_IN_NOTE, new Action().type(MARK_AS_STAFF_ONLY).parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertTrue(item.getCirculationNotes().get(0).getStaffOnly());
 
     circulationNote.setStaffOnly(false);
     circulationNote.setNoteType(CirculationNote.NoteTypeEnum.OUT);
 
-    processor.updater(CHECK_OUT_NOTE, new Action().type(MARK_AS_STAFF_ONLY).parameters(List.of(parameter))).apply(extendedItem);
+    processor.updater(CHECK_OUT_NOTE, new Action().type(MARK_AS_STAFF_ONLY).parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertTrue(item.getCirculationNotes().get(0).getStaffOnly());
   }
 
@@ -378,15 +393,15 @@ void testUpdateRemoveMarkAsStaffOnlyForCirculationNotes() {
     var parameter = new Parameter();
     parameter.setKey(ITEM_NOTE_TYPE_ID_KEY);
     parameter.setValue("typeId");
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(CHECK_IN_NOTE, new Action().type(REMOVE_MARK_AS_STAFF_ONLY).parameters(List.of(parameter))).apply(extendedItem);
+    processor.updater(CHECK_IN_NOTE, new Action().type(REMOVE_MARK_AS_STAFF_ONLY).parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertFalse(item.getCirculationNotes().get(0).getStaffOnly());
 
     circulationNote.setStaffOnly(true);
     circulationNote.setNoteType(CirculationNote.NoteTypeEnum.OUT);
 
-    processor.updater(CHECK_OUT_NOTE, new Action().type(REMOVE_MARK_AS_STAFF_ONLY).parameters(List.of(parameter))).apply(extendedItem);
+    processor.updater(CHECK_OUT_NOTE, new Action().type(REMOVE_MARK_AS_STAFF_ONLY).parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertFalse(item.getCirculationNotes().get(0).getStaffOnly());
   }
 
@@ -396,9 +411,9 @@ void testRemoveAdministrativeNotes() {
     var administrativeNote = "administrative note";
     var item = new Item().withAdministrativeNotes(List.of(administrativeNote));
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(REMOVE_ALL)).apply(extendedItem);
+    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(REMOVE_ALL), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertTrue(item.getAdministrativeNotes().isEmpty());
   }
 
@@ -409,14 +424,14 @@ void testRemoveCirculationNotes() {
     var checkOutNote = new CirculationNote().withNoteType(CirculationNote.NoteTypeEnum.OUT);
     var item = new Item().withCirculationNotes(List.of(checkInNote, checkOutNote));
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(CHECK_IN_NOTE, new Action().type(REMOVE_ALL)).apply(extendedItem);
+    processor.updater(CHECK_IN_NOTE, new Action().type(REMOVE_ALL), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getCirculationNotes().size());
     assertEquals(CirculationNote.NoteTypeEnum.OUT, item.getCirculationNotes().get(0).getNoteType());
 
     item.setCirculationNotes(List.of(checkInNote, checkOutNote));
-    processor.updater(CHECK_OUT_NOTE, new Action().type(REMOVE_ALL)).apply(extendedItem);
+    processor.updater(CHECK_OUT_NOTE, new Action().type(REMOVE_ALL), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getCirculationNotes().size());
     assertEquals(CirculationNote.NoteTypeEnum.IN, item.getCirculationNotes().get(0).getNoteType());
   }
@@ -427,10 +442,10 @@ void testRemoveCheckInNoteAndAddCheckOutNoteOfTheSameNoteType() {
     var checkInNote = new CirculationNote().withNoteType(CirculationNote.NoteTypeEnum.IN);
     var item = new Item().withCirculationNotes(List.of(checkInNote));
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(CHECK_IN_NOTE, new Action().type(REMOVE_ALL)).apply(extendedItem);
-    processor.updater(CHECK_OUT_NOTE, new Action().type(ADD_TO_EXISTING)).apply(extendedItem);
+    processor.updater(CHECK_IN_NOTE, new Action().type(REMOVE_ALL), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
+    processor.updater(CHECK_OUT_NOTE, new Action().type(ADD_TO_EXISTING), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getCirculationNotes().size());
     assertEquals(CirculationNote.NoteTypeEnum.OUT, item.getCirculationNotes().get(0).getNoteType());
   }
@@ -444,10 +459,10 @@ void testRemoveItemNoteAndAddItemNoteOfTheSameNoteType() {
     var parameter = new Parameter();
     parameter.setKey(ITEM_NOTE_TYPE_ID_KEY);
     parameter.setValue("typeId1");
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(ITEM_NOTE, new Action().type(FIND_AND_REMOVE_THESE).parameters(List.of(parameter)).initial("Action note")).apply(extendedItem);
-    processor.updater(ITEM_NOTE, new Action().type(ADD_TO_EXISTING).parameters(List.of(parameter))).apply(extendedItem);
+    processor.updater(ITEM_NOTE, new Action().type(FIND_AND_REMOVE_THESE).parameters(List.of(parameter)).initial("Action note"), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
+    processor.updater(ITEM_NOTE, new Action().type(ADD_TO_EXISTING).parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getNotes().size());
     assertEquals("typeId1", item.getNotes().get(0).getItemNoteTypeId());
   }
@@ -462,9 +477,9 @@ void testRemoveItemNotes() {
     var parameter = new Parameter();
     parameter.setKey(ITEM_NOTE_TYPE_ID_KEY);
     parameter.setValue("typeId1");
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(ITEM_NOTE, new Action().type(REMOVE_ALL).parameters(List.of(parameter))).apply(extendedItem);
+    processor.updater(ITEM_NOTE, new Action().type(REMOVE_ALL).parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getNotes().size());
     assertEquals("typeId2", item.getNotes().get(0).getItemNoteTypeId());
   }
@@ -476,13 +491,13 @@ void testAddAdministrativeNotes() {
     var administrativeNote2 = "administrative note 2";
     var item = new Item();
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(ADD_TO_EXISTING).updated(administrativeNote1)).apply(extendedItem);
+    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(ADD_TO_EXISTING).updated(administrativeNote1), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getAdministrativeNotes().size());
     assertEquals(administrativeNote1, item.getAdministrativeNotes().get(0));
 
-    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(ADD_TO_EXISTING).updated(administrativeNote2)).apply(extendedItem);
+    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(ADD_TO_EXISTING).updated(administrativeNote2), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(2, item.getAdministrativeNotes().size());
   }
 
@@ -493,22 +508,22 @@ void testAddCirculationNotes() {
     var checkOutNote = "checkOutNote";
     var item = new Item();
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(CHECK_IN_NOTE, new Action().type(ADD_TO_EXISTING).updated(checkInNote)).apply(extendedItem);
+    processor.updater(CHECK_IN_NOTE, new Action().type(ADD_TO_EXISTING).updated(checkInNote), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getCirculationNotes().size());
     assertEquals(checkInNote, item.getCirculationNotes().get(0).getNote());
     assertEquals(false, item.getCirculationNotes().get(0).getStaffOnly());
     assertEquals(CirculationNote.NoteTypeEnum.IN, item.getCirculationNotes().get(0).getNoteType());
 
-    processor.updater(CHECK_OUT_NOTE, new Action().type(ADD_TO_EXISTING).updated(checkOutNote)).apply(extendedItem);
+    processor.updater(CHECK_OUT_NOTE, new Action().type(ADD_TO_EXISTING).updated(checkOutNote), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(2, item.getCirculationNotes().size());
     assertEquals(checkOutNote, item.getCirculationNotes().get(1).getNote());
     assertEquals(false, item.getCirculationNotes().get(1).getStaffOnly());
     assertEquals(CirculationNote.NoteTypeEnum.OUT, item.getCirculationNotes().get(1).getNoteType());
 
     List<Parameter> params = Collections.singletonList(new Parameter().key(STAFF_ONLY_NOTE_PARAMETER_KEY).value("true"));
-    processor.updater(CHECK_OUT_NOTE, new Action().type(ADD_TO_EXISTING).parameters(params).updated(checkOutNote)).apply(extendedItem);
+    processor.updater(CHECK_OUT_NOTE, new Action().type(ADD_TO_EXISTING).parameters(params).updated(checkOutNote), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(3, item.getCirculationNotes().size());
     assertEquals(checkOutNote, item.getCirculationNotes().get(2).getNote());
     assertEquals(true, item.getCirculationNotes().get(2).getStaffOnly());
@@ -526,16 +541,16 @@ void testAddItemNotes() {
     parameter.setKey(ITEM_NOTE_TYPE_ID_KEY);
     parameter.setValue("typeId1");
 
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(ITEM_NOTE, new Action().type(ADD_TO_EXISTING).parameters(List.of(parameter)).updated(itemNote1)).apply(extendedItem);
+    processor.updater(ITEM_NOTE, new Action().type(ADD_TO_EXISTING).parameters(List.of(parameter)).updated(itemNote1), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
 
     assertEquals(1, item.getNotes().size());
     assertEquals("typeId1", item.getNotes().get(0).getItemNoteTypeId());
     assertEquals(itemNote1, item.getNotes().get(0).getNote());
 
     parameter.setValue("typeId2");
-    processor.updater(ITEM_NOTE, new Action().type(ADD_TO_EXISTING).parameters(List.of(parameter)).updated(itemNote2)).apply(extendedItem);
+    processor.updater(ITEM_NOTE, new Action().type(ADD_TO_EXISTING).parameters(List.of(parameter)).updated(itemNote2), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
 
     assertEquals(2, item.getNotes().size());
     assertEquals("typeId2", item.getNotes().get(1).getItemNoteTypeId());
@@ -549,12 +564,12 @@ void testFindAndRemoveForAdministrativeNotes() {
     var administrativeNote2 = "administrative note 2";
     var item = new Item().withAdministrativeNotes(new ArrayList<>(List.of(administrativeNote1, administrativeNote2)));
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("administrative note")).apply(extendedItem);
+    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("administrative note"), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(2, item.getAdministrativeNotes().size());
 
-    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial(administrativeNote2)).apply(extendedItem);
+    processor.updater(ADMINISTRATIVE_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial(administrativeNote2), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getAdministrativeNotes().size());
     assertEquals(administrativeNote1, item.getAdministrativeNotes().get(0));
   }
@@ -568,17 +583,17 @@ void testFindAndRemoveForCirculationNotes() {
       .withNoteType(CirculationNote.NoteTypeEnum.OUT).withNote("circ note");
     var item = new Item().withCirculationNotes(List.of(checkInNote, checkOutNote));
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(CHECK_OUT_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("note")).apply(extendedItem);
+    processor.updater(CHECK_OUT_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("note"), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(2, item.getCirculationNotes().size());
-    processor.updater(CHECK_OUT_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("circ note")).apply(extendedItem);
+    processor.updater(CHECK_OUT_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("circ note"), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getCirculationNotes().size());
     assertEquals("circ note", item.getCirculationNotes().get(0).getNote());
     assertEquals(CirculationNote.NoteTypeEnum.IN, item.getCirculationNotes().get(0).getNoteType());
 
     item.setCirculationNotes(List.of(checkInNote, checkOutNote));
-    processor.updater(CHECK_IN_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("circ note")).apply(extendedItem);
+    processor.updater(CHECK_IN_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("circ note"), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getCirculationNotes().size());
     assertEquals("circ note", item.getCirculationNotes().get(0).getNote());
     assertEquals(CirculationNote.NoteTypeEnum.OUT, item.getCirculationNotes().get(0).getNoteType());
@@ -595,16 +610,16 @@ void testFindAndRemoveItemNotes() {
     parameter.setValue("typeId1");
     var item = new Item().withNotes(List.of(itemNote1, itemNote2, itemNote3));
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
     processor.updater(ITEM_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("itemNote")
-      .parameters(List.of(parameter))).apply(extendedItem);
+      .parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(3, item.getNotes().size());
     processor.updater(ITEM_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("itemNote1")
-      .parameters(List.of(parameter))).apply(extendedItem);
+      .parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(2, item.getNotes().size());
     processor.updater(ITEM_NOTE, new Action().type(FIND_AND_REMOVE_THESE).initial("itemNote2")
-      .parameters(List.of(parameter))).apply(extendedItem);
+      .parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getNotes().size());
   }
 
@@ -616,10 +631,10 @@ void testFindAndReplaceForAdministrativeNotes() {
     var administrativeNote3 = "administrative note 3";
     var item = new Item().withAdministrativeNotes(new ArrayList<>(List.of(administrativeNote1, administrativeNote2)));
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
     processor.updater(ADMINISTRATIVE_NOTE, new Action().type(FIND_AND_REPLACE)
-      .initial(administrativeNote1).updated(administrativeNote3)).apply(extendedItem);
+      .initial(administrativeNote1).updated(administrativeNote3), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(2, item.getAdministrativeNotes().size());
     assertEquals(administrativeNote3, item.getAdministrativeNotes().get(0));
     assertEquals(administrativeNote2, item.getAdministrativeNotes().get(1));
@@ -634,10 +649,10 @@ void testFindAndReplaceForCirculationNotes() {
       .withNoteType(CirculationNote.NoteTypeEnum.OUT).withNote("note");
     var item = new Item().withCirculationNotes(List.of(checkInNote, checkOutNote));
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
     processor.updater(CHECK_IN_NOTE, new Action().type(FIND_AND_REPLACE)
-      .initial("note").updated("note 2")).apply(extendedItem);
+      .initial("note").updated("note 2"), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(2, item.getCirculationNotes().size());
     assertEquals("note 2", item.getCirculationNotes().get(0).getNote());
     assertEquals(CirculationNote.NoteTypeEnum.IN, item.getCirculationNotes().get(0).getNoteType());
@@ -647,7 +662,7 @@ void testFindAndReplaceForCirculationNotes() {
     checkInNote.setNote("note");
 
     processor.updater(CHECK_OUT_NOTE, new Action().type(FIND_AND_REPLACE)
-      .initial("note").updated("note 2")).apply(extendedItem);
+      .initial("note").updated("note 2"), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(2, item.getCirculationNotes().size());
     assertEquals("note", item.getCirculationNotes().get(0).getNote());
     assertEquals(CirculationNote.NoteTypeEnum.IN, item.getCirculationNotes().get(0).getNoteType());
@@ -665,10 +680,10 @@ void testFindAndReplaceForItemNotes() {
     parameter.setValue("typeId1");
     var item = new Item().withNotes(List.of(itemNote1, itemNote2));
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
     processor.updater(ITEM_NOTE, new Action().type(FIND_AND_REPLACE).parameters(List.of(parameter))
-      .initial("itemNote1").updated("itemNote3")).apply(extendedItem);
+      .initial("itemNote1").updated("itemNote3"), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
 
     assertEquals("itemNote3", item.getNotes().get(0).getNote());
     assertEquals("itemNote1", item.getNotes().get(1).getNote());
@@ -680,10 +695,10 @@ void testChangeTypeForAdministrativeNotes() {
     var administrativeNote = "note";
     var item = new Item().withAdministrativeNotes(new ArrayList<>(List.of(administrativeNote)));
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
     processor.updater(ADMINISTRATIVE_NOTE, new Action().type(CHANGE_TYPE)
-      .updated(CHECK_IN_NOTE_TYPE)).apply(extendedItem);
+      .updated(CHECK_IN_NOTE_TYPE), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(0, item.getAdministrativeNotes().size());
     assertEquals(1, item.getCirculationNotes().size());
     assertEquals("note", item.getCirculationNotes().get(0).getNote());
@@ -693,7 +708,7 @@ void testChangeTypeForAdministrativeNotes() {
     item.setAdministrativeNotes(List.of(administrativeNote));
 
     processor.updater(ADMINISTRATIVE_NOTE, new Action().type(CHANGE_TYPE)
-      .updated(CHECK_OUT_NOTE_TYPE)).apply(extendedItem);
+      .updated(CHECK_OUT_NOTE_TYPE), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(0, item.getAdministrativeNotes().size());
     assertEquals(1, item.getCirculationNotes().size());
     assertEquals("note", item.getCirculationNotes().get(0).getNote());
@@ -703,7 +718,7 @@ void testChangeTypeForAdministrativeNotes() {
     item.setAdministrativeNotes(List.of(administrativeNote));
 
     processor.updater(ADMINISTRATIVE_NOTE, new Action().type(CHANGE_TYPE)
-      .updated("typeId")).apply(extendedItem);
+      .updated("typeId"), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
 
     assertEquals(0, item.getAdministrativeNotes().size());
     assertEquals(1, item.getNotes().size());
@@ -720,10 +735,10 @@ void testChangeNoteTypeForCirculationNotes() {
       .withNoteType(CirculationNote.NoteTypeEnum.OUT).withNote("note 2").withStaffOnly(true);
     var item = new Item().withCirculationNotes(List.of(checkInNote, checkOutNote));
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
     processor.updater(CHECK_IN_NOTE, new Action().type(CHANGE_TYPE)
-      .updated(CHECK_OUT_NOTE_TYPE)).apply(extendedItem);
+      .updated(CHECK_OUT_NOTE_TYPE), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(2, item.getCirculationNotes().size());
     assertEquals("note", item.getCirculationNotes().get(0).getNote());
     assertEquals(CirculationNote.NoteTypeEnum.OUT, item.getCirculationNotes().get(0).getNoteType());
@@ -732,7 +747,7 @@ void testChangeNoteTypeForCirculationNotes() {
     checkInNote.setNoteType(CirculationNote.NoteTypeEnum.IN);
 
     processor.updater(CHECK_IN_NOTE, new Action().type(CHANGE_TYPE)
-      .updated(ADMINISTRATIVE_NOTE_TYPE)).apply(extendedItem);
+      .updated(ADMINISTRATIVE_NOTE_TYPE), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getCirculationNotes().size());
     assertEquals(CirculationNote.NoteTypeEnum.OUT, item.getCirculationNotes().get(0).getNoteType());
     assertEquals(1, item.getAdministrativeNotes().size());
@@ -742,7 +757,7 @@ void testChangeNoteTypeForCirculationNotes() {
     item.setCirculationNotes(List.of(checkInNote, checkOutNote));
 
     processor.updater(CHECK_IN_NOTE, new Action().type(CHANGE_TYPE)
-      .updated("typeId")).apply(extendedItem);
+      .updated("typeId"), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getCirculationNotes().size());
     assertEquals(CirculationNote.NoteTypeEnum.OUT, item.getCirculationNotes().get(0).getNoteType());
     assertEquals(1, item.getNotes().size());
@@ -761,9 +776,9 @@ void testChangeNoteTypeForItemNotes() {
     parameter.setValue("typeId1");
     var item = new Item().withNotes(List.of(itemNote1, itemNote2));
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(ITEM_NOTE, new Action().type(CHANGE_TYPE).updated(ADMINISTRATIVE_NOTE_TYPE).parameters(List.of(parameter))).apply(extendedItem);
+    processor.updater(ITEM_NOTE, new Action().type(CHANGE_TYPE).updated(ADMINISTRATIVE_NOTE_TYPE).parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
 
     assertEquals(1, item.getAdministrativeNotes().size());
     assertEquals("itemNote1", item.getAdministrativeNotes().get(0));
@@ -773,7 +788,7 @@ void testChangeNoteTypeForItemNotes() {
     item.setAdministrativeNotes(null);
     item.setNotes(List.of(itemNote1, itemNote2));
 
-    processor.updater(ITEM_NOTE, new Action().type(CHANGE_TYPE).updated(CHECK_IN_NOTE_TYPE).parameters(List.of(parameter))).apply(extendedItem);
+    processor.updater(ITEM_NOTE, new Action().type(CHANGE_TYPE).updated(CHECK_IN_NOTE_TYPE).parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getCirculationNotes().size());
     assertEquals("itemNote1", item.getCirculationNotes().get(0).getNote());
     assertEquals(CirculationNote.NoteTypeEnum.IN, item.getCirculationNotes().get(0).getNoteType());
@@ -784,7 +799,7 @@ void testChangeNoteTypeForItemNotes() {
     item.setCirculationNotes(null);
     item.setNotes(List.of(itemNote1, itemNote2));
 
-    processor.updater(ITEM_NOTE, new Action().type(CHANGE_TYPE).updated(CHECK_OUT_NOTE_TYPE).parameters(List.of(parameter))).apply(extendedItem);
+    processor.updater(ITEM_NOTE, new Action().type(CHANGE_TYPE).updated(CHECK_OUT_NOTE_TYPE).parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(1, item.getCirculationNotes().size());
     assertEquals("itemNote1", item.getCirculationNotes().get(0).getNote());
     assertEquals(CirculationNote.NoteTypeEnum.OUT, item.getCirculationNotes().get(0).getNoteType());
@@ -795,7 +810,7 @@ void testChangeNoteTypeForItemNotes() {
     item.setCirculationNotes(null);
     item.setNotes(List.of(itemNote1, itemNote2));
 
-    processor.updater(ITEM_NOTE, new Action().type(CHANGE_TYPE).updated("typeId3").parameters(List.of(parameter))).apply(extendedItem);
+    processor.updater(ITEM_NOTE, new Action().type(CHANGE_TYPE).updated("typeId3").parameters(List.of(parameter)), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(2, item.getNotes().size());
     assertEquals("itemNote1", item.getNotes().get(0).getNote());
     assertEquals("typeId3", item.getNotes().get(0).getItemNoteTypeId());
@@ -812,9 +827,9 @@ void testDuplicateForCirculationNotes() {
       .withNoteType(CirculationNote.NoteTypeEnum.OUT).withNote("note 2").withStaffOnly(true);
     var item = new Item().withCirculationNotes(new ArrayList<>(List.of(checkInNote, checkOutNote)));
     var extendedItem = ExtendedItem.builder().entity(item).tenantId("tenant").build();
-    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(null, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
 
-    processor.updater(CHECK_IN_NOTE, new Action().type(DUPLICATE).updated(CHECK_OUT_NOTE_TYPE)).apply(extendedItem);
+    processor.updater(CHECK_IN_NOTE, new Action().type(DUPLICATE).updated(CHECK_OUT_NOTE_TYPE), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(3, item.getCirculationNotes().size());
     assertEquals(2, item.getCirculationNotes().stream().filter(circNote -> circNote.getNoteType() == CirculationNote.NoteTypeEnum.OUT).count());
     var duplicated = item.getCirculationNotes().stream().filter(circNote ->
@@ -822,7 +837,7 @@ void testDuplicateForCirculationNotes() {
     assertTrue(duplicated.isPresent());
     assertTrue(duplicated.get().getStaffOnly());
 
-    processor.updater(CHECK_OUT_NOTE, new Action().type(DUPLICATE).updated(CHECK_IN_NOTE_TYPE)).apply(extendedItem);
+    processor.updater(CHECK_OUT_NOTE, new Action().type(DUPLICATE).updated(CHECK_IN_NOTE_TYPE), extendedItem, new BulkOperationRule().ruleDetails(new BulkOperationRuleRuleDetails())).apply(extendedItem);
     assertEquals(5, item.getCirculationNotes().size());
     assertEquals(3, item.getCirculationNotes().stream().filter(circNote -> circNote.getNoteType() == CirculationNote.NoteTypeEnum.IN).count());
 
@@ -838,7 +853,7 @@ void testDuplicateForCirculationNotes() {
 
   @Test
   void testClone() {
-    var processor = new ItemDataProcessor(holdingsReferenceService, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()), folioExecutionContext);
+    var processor = new ItemDataProcessor(holdingsReferenceService, null, new ItemsNotesUpdater(new AdministrativeNotesUpdater()));
     var administrativeNotes = new ArrayList<String>();
     administrativeNotes.add("note1");
     var item1 = new Item().withId("id")
@@ -883,4 +898,81 @@ void testClone() {
 
     assertFalse(processor.compare(extendedItem1, extendedItem2));
   }
+
+  @Test
+  void testShouldNotUpdateItemWithLoanType_whenLoanTypeFromOtherTenantThanActionTenants() {
+    when(folioExecutionContext.getTenantId()).thenReturn("memberB");
+    when(consortiaService.getCentralTenantId("memberB")).thenReturn("central");
+
+    try (var ignored = Mockito.mockStatic(FolioExecutionContextUtil.class)) {
+      when(FolioExecutionContextUtil.prepareContextForTenant(any(), any(), any())).thenReturn(folioExecutionContext);
+
+      var loanTypeFromMemberB = UUID.randomUUID().toString();
+      var actionTenants = List.of("memberB");
+      var itemId = UUID.randomUUID().toString();
+      var initPermanentLoanTypeId = UUID.randomUUID().toString();
+      var extendedItem = ExtendedItem.builder().entity(new Item().withId(itemId).withPermanentLoanType(new LoanType().withId(initPermanentLoanTypeId))).tenantId("memberA").build();
+
+      var rules = rules(rule(PERMANENT_LOAN_TYPE, REPLACE_WITH, loanTypeFromMemberB, actionTenants, List.of()));
+      var operationId = rules.getBulkOperationRules().get(0).getBulkOperationId();
+
+      var result = processor.process(IDENTIFIER, extendedItem, rules);
+
+      assertNotNull(result);
+      assertEquals(initPermanentLoanTypeId, result.getUpdated().getEntity().getPermanentLoanType().getId());
+
+      verify(errorService, times(1)).saveError(operationId, IDENTIFIER, String.format("%s cannot be updated because the record is associated with %s and %s is not associated with this tenant.",
+        itemId, "memberA", "PERMANENT_LOAN_TYPE").trim());
+    }
+  }
+
+  @Test
+  void testShouldNotUpdateItemWithLoanType_whenLoanTypeFromOtherTenantThanRuleTenants() {
+    when(folioExecutionContext.getTenantId()).thenReturn("memberB");
+    when(consortiaService.getCentralTenantId("memberB")).thenReturn("central");
+
+    try (var ignored = Mockito.mockStatic(FolioExecutionContextUtil.class)) {
+      when(FolioExecutionContextUtil.prepareContextForTenant(any(), any(), any())).thenReturn(folioExecutionContext);
+
+      var adminNoteFromMemberB = UUID.randomUUID().toString();
+      var ruleTenants = List.of("memberB");
+      var itemId = UUID.randomUUID().toString();
+      var initPermanentLoanTypeId = UUID.randomUUID().toString();
+      var extendedItem = ExtendedItem.builder().entity(new Item().withId(itemId).withPermanentLoanType(new LoanType().withId(initPermanentLoanTypeId))).tenantId("memberA").build();
+
+      var rules = rules(rule(PERMANENT_LOAN_TYPE, REPLACE_WITH, adminNoteFromMemberB, List.of(), ruleTenants));
+      var operationId = rules.getBulkOperationRules().get(0).getBulkOperationId();
+
+      var result = processor.process(IDENTIFIER, extendedItem, rules);
+
+      assertNotNull(result);
+      assertEquals(initPermanentLoanTypeId, result.getUpdated().getEntity().getPermanentLoanType().getId());
+
+      verify(errorService, times(1)).saveError(operationId, IDENTIFIER, String.format("%s cannot be updated because the record is associated with %s and %s is not associated with this tenant.",
+        itemId, "memberA", "PERMANENT_LOAN_TYPE").trim());
+    }
+  }
+
+  @Test
+  void testShouldUpdateItemWithLoanType_whenLoanTypeFromTenantAmongRuleTenants() {
+
+    var permanentLoanTypeFromMemberB = UUID.randomUUID().toString();
+
+    when(loanTypeClient.getLoanTypeById(permanentLoanTypeFromMemberB)).thenReturn(new LoanType().withId(permanentLoanTypeFromMemberB));
+    when(itemReferenceService.getLoanTypeById(permanentLoanTypeFromMemberB)).thenReturn(new LoanType().withId(permanentLoanTypeFromMemberB));
+
+    var ruleTenants = List.of("memberB", "memberA");
+    var itemId = UUID.randomUUID().toString();
+    var initPermanentLoanTypeId = UUID.randomUUID().toString();
+    var extendedItem = ExtendedItem.builder().entity(new Item().withId(itemId).withPermanentLoanType(new LoanType().withId(initPermanentLoanTypeId))).tenantId("memberA").build();
+
+    var rules = rules(rule(PERMANENT_LOAN_TYPE, REPLACE_WITH, permanentLoanTypeFromMemberB, List.of(), ruleTenants));
+
+    var result = processor.process(IDENTIFIER, extendedItem, rules);
+
+    assertNotNull(result);
+    assertEquals(permanentLoanTypeFromMemberB, result.getUpdated().getEntity().getPermanentLoanType().getId());
+
+    verifyNoInteractions(errorService);
+  }
 }