Repository: folio-org/mod-bulk-operations
Pull request #389: MODBULKOPS-390 - Previewing bulk edit changes
Author: login: siarhei-charniak
Created: 2025-03-13T19:10:14Z | Merged: 2025-03-17T12:00:39Z

[MODBULKOPS-390](https://folio-org.atlassian.net/browse/MODBULKOPS-390) - Previewing bulk edit changes

* Fixed issues

## Pre-Merge Checklist:
Before merging this PR, please go through the following list and take appropriate actions.

- Does this PR meet or exceed the expected quality standards?
  - [x] Code coverage on new code is 80% or greater
  - [x] Duplications on new code is 3% or less
  - [x] There are no major code smells or security issues
- Does this introduce breaking changes?
  - [ ] Were any API paths or methods changed, added or removed?
  - [ ] Were there any schema changes?
  - [ ] Did any of the interface versions change?
  - [ ] Were permissions changed, added, or removed?
  - [ ] Are there new interface dependencies?
  - [x] There are no breaking changes in this PR.

If there are breaking changes, please **STOP** and consider the following:

- What other modules will these changes impact?
- Do JIRAs exist to update the impacted modules?
  - [ ] If not, please create them
  - [ ] Do they contain the appropriate level of detail?  Which endpoints/schemas changed, etc.
  - [ ] Do they have all they appropriate links to blocked/related issues?
- Are the JIRAs under active development?
  - [ ] If not, contact the project's PO and make sure they're aware of the urgency.
- Do PRs exist for these changes?
  - [ ] If so, have they been approved?

Ideally all of the PRs involved in breaking changes would be merged in the same day to avoid breaking the folio-testing environment.  Communication is paramount if that is to be achieved, especially as the number of intermodule and inter-team dependencies increase.

While it's helpful for reviewers to help identify potential problems, ensuring that it's safe to merge is ultimately the responsibility of the PR assignee.



Diff:
diff --git a/src/main/java/org/folio/bulkops/processor/CommitProcessor.java b/src/main/java/org/folio/bulkops/processor/CommitProcessor.java
new file mode 100644
index 000000000..bdf7ab25e
--- /dev/null
+++ b/src/main/java/org/folio/bulkops/processor/CommitProcessor.java
@@ -0,0 +1,7 @@
+package org.folio.bulkops.processor;
+
+import org.folio.bulkops.domain.entity.BulkOperation;
+
+public interface CommitProcessor {
+  void processCommit(BulkOperation bulkOperation);
+}
diff --git a/src/main/java/org/folio/bulkops/processor/marc/MarcFlowCommitProcessor.java b/src/main/java/org/folio/bulkops/processor/marc/MarcFlowCommitProcessor.java
new file mode 100644
index 000000000..042388445
--- /dev/null
+++ b/src/main/java/org/folio/bulkops/processor/marc/MarcFlowCommitProcessor.java
@@ -0,0 +1,181 @@
+package org.folio.bulkops.processor.marc;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.isNull;
+import static java.util.Objects.nonNull;
+import static org.apache.commons.io.FilenameUtils.getBaseName;
+import static org.folio.bulkops.domain.bean.Instance.INSTANCE_HRID;
+import static org.folio.bulkops.util.Constants.CHANGED_CSV_PATH_TEMPLATE;
+import static org.folio.bulkops.util.Constants.CHANGED_MARC_PATH_TEMPLATE;
+import static org.folio.bulkops.util.Constants.ENRICHED_PREFIX;
+import static org.folio.bulkops.util.Constants.LINE_BREAK;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.opencsv.CSVReaderBuilder;
+import com.opencsv.exceptions.CsvDataTypeMismatchException;
+import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
+import com.opencsv.exceptions.CsvValidationException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.apache.commons.collections4.CollectionUtils;
+import org.folio.bulkops.client.RemoteFileSystemClient;
+import org.folio.bulkops.domain.bean.BulkOperationsEntity;
+import org.folio.bulkops.domain.bean.ExtendedInstance;
+import org.folio.bulkops.domain.bean.Instance;
+import org.folio.bulkops.domain.converter.BulkOperationsEntityCsvWriter;
+import org.folio.bulkops.domain.entity.BulkOperation;
+import org.folio.bulkops.exception.ConverterException;
+import org.folio.bulkops.processor.CommitProcessor;
+import org.folio.bulkops.util.CSVHelper;
+import org.folio.bulkops.util.UnifiedTableHeaderBuilder;
+import org.marc4j.MarcStreamReader;
+import org.marc4j.MarcStreamWriter;
+import org.springframework.stereotype.Service;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.SequenceInputStream;
+import java.io.StringWriter;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+@Log4j2
+public class MarcFlowCommitProcessor implements CommitProcessor {
+  private final RemoteFileSystemClient remoteFileSystemClient;
+  private final ObjectMapper objectMapper;
+
+  @Override
+  public void processCommit(BulkOperation bulkOperation) {
+    var csvHrids = getUpdatedInventoryInstanceHrids(bulkOperation);
+    var marcHrids = getUpdatedMarcInstanceHrids(bulkOperation);
+
+    var updatedHrids = new HashSet<>(csvHrids);
+    updatedHrids.addAll(marcHrids);
+    bulkOperation.setCommittedNumOfRecords(updatedHrids.size());
+
+    enrichCommittedCsvWithUpdatedMarcRecords(bulkOperation, csvHrids, marcHrids);
+    enrichCommittedMarcWithUpdatedInventoryRecords(bulkOperation, csvHrids, marcHrids);
+  }
+
+  public void enrichCommittedCsvWithUpdatedMarcRecords(BulkOperation bulkOperation, List<String> csvHrids, List<String> marcHrids) {
+    var hrids = CollectionUtils.subtract(marcHrids, csvHrids);
+    if (!hrids.isEmpty() && nonNull(bulkOperation.getLinkToMatchedRecordsJsonFile())) {
+      var dirName = isNull(bulkOperation.getLinkToCommittedRecordsCsvFile()) ? bulkOperation.getId().toString() : ENRICHED_PREFIX + bulkOperation.getId();
+      var committedCsvFileName = CHANGED_CSV_PATH_TEMPLATE.formatted(dirName, LocalDate.now(), getBaseName(bulkOperation.getLinkToTriggeringCsvFile()));
+      try (var matchedFileReader = new InputStreamReader(new BufferedInputStream(remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsJsonFile())));
+           var writer = isNull(bulkOperation.getLinkToCommittedRecordsCsvFile()) ?
+             remoteFileSystemClient.writer(committedCsvFileName) : new StringWriter()) {
+        var matchedFileParser = new JsonFactory().createParser(matchedFileReader);
+        var matchedFileIterator = objectMapper.readValues(matchedFileParser, ExtendedInstance.class);
+        var csvWriter = new BulkOperationsEntityCsvWriter(writer, Instance.class);
+        while (matchedFileIterator.hasNext()) {
+          var instance = matchedFileIterator.next().getEntity();
+          if (hrids.contains(instance.getHrid())) {
+            writeBeanToCsv(csvWriter, instance);
+          }
+        }
+        if (nonNull(bulkOperation.getLinkToCommittedRecordsCsvFile())) {
+          var appendedCsvRecords = writer.toString();
+          appendedCsvRecords = appendedCsvRecords.substring(appendedCsvRecords.indexOf(LINE_BREAK) + 1);
+          var appendedCsvStream = new SequenceInputStream(
+            remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsCsvFile()),
+            new ByteArrayInputStream(appendedCsvRecords.getBytes()));
+          remoteFileSystemClient.put(appendedCsvStream, committedCsvFileName);
+          remoteFileSystemClient.remove(bulkOperation.getLinkToCommittedRecordsCsvFile());
+          bulkOperation.setLinkToCommittedRecordsCsvFile(committedCsvFileName);
+        }
+        if (nonNull(bulkOperation.getLinkToCommittedRecordsCsvFile())) {
+          remoteFileSystemClient.remove(bulkOperation.getLinkToCommittedRecordsCsvFile());
+        }
+        bulkOperation.setLinkToCommittedRecordsCsvFile(committedCsvFileName);
+      } catch (IOException | CsvRequiredFieldEmptyException | CsvDataTypeMismatchException e) {
+        log.error("Failed to enrich csv file", e);
+      }
+    }
+  }
+
+  private void writeBeanToCsv(BulkOperationsEntityCsvWriter writer, BulkOperationsEntity bean)
+    throws CsvRequiredFieldEmptyException, CsvDataTypeMismatchException {
+    try {
+      writer.write(bean);
+    } catch (ConverterException e) {
+      writeBeanToCsv(writer, bean);
+    }
+  }
+
+  public void enrichCommittedMarcWithUpdatedInventoryRecords(BulkOperation bulkOperation, List<String> csvHrids, List<String> marcHrids) {
+    var hrids = CollectionUtils.subtract(csvHrids, marcHrids);
+    if (!hrids.isEmpty() && nonNull(bulkOperation.getLinkToMatchedRecordsMarcFile())) {
+      var linkToCommitted = bulkOperation.getLinkToCommittedRecordsMarcFile();
+      var dirName = isNull(linkToCommitted) ? bulkOperation.getId().toString() : ENRICHED_PREFIX + bulkOperation.getId();
+      var committedMarcFileName = CHANGED_MARC_PATH_TEMPLATE.formatted(dirName, LocalDate.now(), getBaseName(bulkOperation.getLinkToTriggeringCsvFile()));
+      try (var matchedMarcInputStream = remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsMarcFile());
+           var committedMarcInputStream = isNull(linkToCommitted) ? null : remoteFileSystemClient.get(linkToCommitted);
+           var marcOutputStream = new ByteArrayOutputStream()) {
+        var marcReader = new MarcStreamReader(matchedMarcInputStream);
+        var streamWriter = new MarcStreamWriter(marcOutputStream, "UTF-8");
+        while (marcReader.hasNext()) {
+          var marcRecord = marcReader.next();
+          if (hrids.contains(marcRecord.getControlNumber())) {
+            streamWriter.write(marcRecord);
+          }
+        }
+        var appendedMarcStream = new ByteArrayInputStream(marcOutputStream.toString(UTF_8).getBytes());
+        var committedMarcStream = isNull(linkToCommitted) ?
+          appendedMarcStream :
+          new SequenceInputStream(committedMarcInputStream, appendedMarcStream);
+        remoteFileSystemClient.put(committedMarcStream, committedMarcFileName);
+        if (nonNull(bulkOperation.getLinkToCommittedRecordsMarcFile())) {
+          remoteFileSystemClient.remove(bulkOperation.getLinkToCommittedRecordsMarcFile());
+        }
+        bulkOperation.setLinkToCommittedRecordsMarcFile(committedMarcFileName);
+      } catch (IOException e) {
+        log.error("Failed to enrich marc file", e);
+      }
+    }
+  }
+
+  public List<String> getUpdatedInventoryInstanceHrids(BulkOperation bulkOperation) {
+    List<String> updatedHrids = new ArrayList<>();
+    if (nonNull(bulkOperation.getLinkToCommittedRecordsCsvFile())) {
+      var instanceHeaderNames = UnifiedTableHeaderBuilder.getEmptyTableWithHeaders(Instance.class).getHeader().stream()
+        .map(org.folio.bulkops.domain.dto.Cell::getValue)
+        .toList();
+      try (var reader = new CSVReaderBuilder(new InputStreamReader(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsCsvFile())))
+        .withCSVParser(CSVHelper.getCsvParser()).withSkipLines(1).build()) {
+        String[] line;
+        while ((line = reader.readNext()) != null) {
+          if (line.length == instanceHeaderNames.size()) {
+            updatedHrids.add(line[instanceHeaderNames.indexOf(INSTANCE_HRID)]);
+          }
+        }
+      } catch (IOException | CsvValidationException e) {
+        log.error("Failed to read csv file", e);
+      }
+    }
+    return updatedHrids;
+  }
+
+  public List<String> getUpdatedMarcInstanceHrids(BulkOperation bulkOperation) {
+    List<String> updatedHrids = new ArrayList<>();
+    if (nonNull(bulkOperation.getLinkToCommittedRecordsMarcFile())) {
+      try (var marcInputStream = remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsMarcFile())) {
+        var marcReader = new MarcStreamReader(marcInputStream);
+        while (marcReader.hasNext()) {
+          var marcRecord = marcReader.next();
+          updatedHrids.add(marcRecord.getControlNumber());
+        }
+      } catch (IOException e) {
+        log.error("Failed to read marc file", e);
+      }
+    }
+    return updatedHrids;
+  }
+}
diff --git a/src/main/java/org/folio/bulkops/processor/marc/MarcInstanceUpdateProcessor.java b/src/main/java/org/folio/bulkops/processor/marc/MarcInstanceUpdateProcessor.java
index b26c2bb75..e04da4fa6 100644
--- a/src/main/java/org/folio/bulkops/processor/marc/MarcInstanceUpdateProcessor.java
+++ b/src/main/java/org/folio/bulkops/processor/marc/MarcInstanceUpdateProcessor.java
@@ -3,9 +3,6 @@
 import static java.lang.Boolean.TRUE;
 import static org.apache.commons.lang3.StringUtils.EMPTY;
 import static org.apache.commons.lang3.StringUtils.isEmpty;
-import static org.folio.bulkops.domain.dto.OperationStatusType.COMPLETED;
-import static org.folio.bulkops.domain.dto.OperationStatusType.COMPLETED_WITH_ERRORS;
-import static org.folio.bulkops.util.Constants.ERROR_COMMITTING_FILE_NAME_PREFIX;
 import static org.folio.bulkops.util.Constants.MARC;
 
 import lombok.RequiredArgsConstructor;
@@ -32,12 +29,11 @@
 import org.folio.bulkops.domain.entity.BulkOperation;
 import org.folio.bulkops.processor.MarcUpdateProcessor;
 import org.folio.bulkops.repository.BulkOperationRepository;
-import org.folio.bulkops.service.ErrorService;
+import org.folio.bulkops.service.BulkOperationServiceHelper;
 import org.folio.bulkops.util.MarcDateHelper;
 import org.springframework.stereotype.Component;
 
 import java.io.IOException;
-import java.time.LocalDateTime;
 import java.util.Date;
 import java.util.List;
 import java.util.UUID;
@@ -55,7 +51,7 @@ public class MarcInstanceUpdateProcessor implements MarcUpdateProcessor {
   private final RemoteFileSystemClient remoteFileSystemClient;
   private final BulkOperationRepository bulkOperationRepository;
   private final DataImportRestS3UploadClient dataImportRestS3UploadClient;
-  private final ErrorService errorService;
+  private final BulkOperationServiceHelper bulkOperationServiceHelper;
 
   public void updateMarcRecords(BulkOperation bulkOperation) throws IOException {
     try (var is = remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsMarcFile())) {
@@ -69,15 +65,11 @@ public void updateMarcRecords(BulkOperation bulkOperation) throws IOException {
             .build(),
           uploadDefinition.getId());
         bulkOperation.setDataImportJobProfileId(UUID.fromString(jobProfile.getId()));
+        bulkOperationRepository.save(bulkOperation);
       } else {
         bulkOperation.setLinkToCommittedRecordsMarcFile(null);
-        bulkOperation.setLinkToCommittedRecordsErrorsCsvFile(errorService.uploadErrorsToStorage(bulkOperation.getId(), ERROR_COMMITTING_FILE_NAME_PREFIX, null));
-        bulkOperation.setCommittedNumOfErrors(errorService.getCommittedNumOfErrors(bulkOperation.getId()));
-        bulkOperation.setCommittedNumOfWarnings(errorService.getCommittedNumOfWarnings(bulkOperation.getId()));
-        bulkOperation.setEndTime(LocalDateTime.now());
-        bulkOperation.setStatus(bulkOperation.getCommittedNumOfErrors() == 0 ? COMPLETED : COMPLETED_WITH_ERRORS);
+        bulkOperationServiceHelper.completeBulkOperation(bulkOperation);
       }
-      bulkOperationRepository.save(bulkOperation);
     }
   }
 
diff --git a/src/main/java/org/folio/bulkops/service/BulkOperationService.java b/src/main/java/org/folio/bulkops/service/BulkOperationService.java
index baa4a098d..d59144a56 100644
--- a/src/main/java/org/folio/bulkops/service/BulkOperationService.java
+++ b/src/main/java/org/folio/bulkops/service/BulkOperationService.java
@@ -5,7 +5,6 @@
 import static java.util.Objects.nonNull;
 import static org.apache.commons.lang3.StringUtils.EMPTY;
 import static org.apache.commons.lang3.StringUtils.LF;
-import static org.apache.commons.lang3.StringUtils.isEmpty;
 import static org.folio.bulkops.domain.dto.ApproachType.IN_APP;
 import static org.folio.bulkops.domain.dto.ApproachType.MANUAL;
 import static org.folio.bulkops.domain.dto.ApproachType.QUERY;
@@ -13,8 +12,6 @@
 import static org.folio.bulkops.domain.dto.EntityType.INSTANCE_MARC;
 import static org.folio.bulkops.domain.dto.IdentifierType.HRID;
 import static org.folio.bulkops.domain.dto.OperationStatusType.APPLY_CHANGES;
-import static org.folio.bulkops.domain.dto.OperationStatusType.COMPLETED;
-import static org.folio.bulkops.domain.dto.OperationStatusType.COMPLETED_WITH_ERRORS;
 import static org.folio.bulkops.domain.dto.OperationStatusType.DATA_MODIFICATION;
 import static org.folio.bulkops.domain.dto.OperationStatusType.DATA_MODIFICATION_IN_PROGRESS;
 import static org.folio.bulkops.domain.dto.OperationStatusType.EXECUTING_QUERY;
@@ -24,7 +21,7 @@
 import static org.folio.bulkops.domain.dto.OperationStatusType.REVIEW_CHANGES;
 import static org.folio.bulkops.domain.dto.OperationStatusType.SAVED_IDENTIFIERS;
 import static org.folio.bulkops.domain.dto.OperationStatusType.SAVING_RECORDS_LOCALLY;
-import static org.folio.bulkops.util.Constants.COMMA_DELIMETER;
+import static org.folio.bulkops.util.Constants.CHANGED_CSV_PATH_TEMPLATE;
 import static org.folio.bulkops.util.Constants.ERROR_COMMITTING_FILE_NAME_PREFIX;
 import static org.folio.bulkops.util.Constants.ERROR_MATCHING_FILE_NAME_PREFIX;
 import static org.folio.bulkops.util.Constants.FIELD_ERROR_MESSAGE_PATTERN;
@@ -157,6 +154,7 @@ public class BulkOperationService {
   private final MetadataProviderService metadataProviderService;
   private final SrsService srsService;
   private final MarcCsvHelper marcCsvHelper;
+  private final BulkOperationServiceHelper bulkOperationServiceHelper;
 
   private static final int OPERATION_UPDATING_STEP = 100;
   private static final String PREVIEW_JSON_PATH_TEMPLATE = "%s/json/%s-Updates-Preview-%s.json";
@@ -164,7 +162,7 @@ public class BulkOperationService {
   private static final String PREVIEW_MARC_PATH_TEMPLATE = "%s/%s-Updates-Preview-MARC-%s.mrc";
   private static final String PREVIEW_MARC_CSV_PATH_TEMPLATE = "%s/%s-Updates-Preview-MARC-CSV-%s.csv";
   private static final String CHANGED_JSON_PATH_TEMPLATE = "%s/json/%s-Changed-Records-%s.json";
-  public static final String CHANGED_CSV_PATH_TEMPLATE = "%s/%s-Changed-Records-CSV-%s.csv";
+
   public static final String TMP_MATCHED_JSON_PATH_TEMPLATE = "%s/json/tmp-matched.json";
 
   private final ExecutorService executor = Executors.newCachedThreadPool();
@@ -521,42 +519,20 @@ public void commit(BulkOperation operation) {
         execution = execution
           .withStatus(StatusType.FAILED)
           .withEndTime(LocalDateTime.now());
-        operation.setStatus(OperationStatusType.FAILED);
-        operation.setEndTime(LocalDateTime.now());
-        operation.setErrorMessage(e.getMessage());
-        var linkToCommittingErrorsFile = errorService.uploadErrorsToStorage(operation.getId(), ERROR_COMMITTING_FILE_NAME_PREFIX, operation.getErrorMessage());
-        operation.setLinkToCommittedRecordsErrorsCsvFile(linkToCommittingErrorsFile);
+        bulkOperationServiceHelper.failCommit(operation, e);
       }
       executionRepository.save(execution);
     }
 
     if (!INSTANCE_MARC.equals(operation.getEntityType()) || !hasMarcRules) {
-      operation.setProcessedNumOfRecords(operation.getCommittedNumOfRecords());
-      operation.setEndTime(LocalDateTime.now());
-
-      var linkToCommittingErrorsFile = errorService.uploadErrorsToStorage(operationId, ERROR_COMMITTING_FILE_NAME_PREFIX, null);
-      operation.setLinkToCommittedRecordsErrorsCsvFile(linkToCommittingErrorsFile);
-      operation.setCommittedNumOfErrors(errorService.getCommittedNumOfErrors(operationId));
-      operation.setCommittedNumOfWarnings(errorService.getCommittedNumOfWarnings(operationId));
-
       if (!FAILED.equals(operation.getStatus())) {
-        operation.setStatus(isEmpty(linkToCommittingErrorsFile) ? COMPLETED : COMPLETED_WITH_ERRORS);
-        processCommittedFiles(operation);
-      } else {
-        operation.setLinkToCommittedRecordsCsvFile(null);
+        bulkOperationServiceHelper.completeBulkOperation(operation);
       }
-      bulkOperationRepository.save(operation);
     } else {
       marcUpdateService.commitForInstanceMarc(operation);
     }
   }
 
-  private void processCommittedFiles(BulkOperation bulkOperation) {
-    if (INSTANCE_MARC.equals(bulkOperation.getEntityType())) {
-      marcCsvHelper.enrichMarcAndCsvCommittedFiles(bulkOperation);
-    }
-  }
-
   private boolean isCurrentTenantNotCentral(String tenantId) {
     return !consortiaService.isTenantCentral(tenantId);
   }
@@ -873,9 +849,6 @@ private synchronized void handleProcessingCompletion(UUID operationId) {
 
   private synchronized void saveLinks(BulkOperation source) {
     var dest = getBulkOperationOrThrow(source.getId());
-    if (nonNull(source.getLinkToMatchedRecordsJsonFile())) {
-      dest.setLinkToMatchedRecordsJsonFile(source.getLinkToMatchedRecordsJsonFile());
-    }
     if (nonNull(source.getLinkToModifiedRecordsCsvFile())) {
       dest.setLinkToModifiedRecordsCsvFile(source.getLinkToModifiedRecordsCsvFile());
     }
diff --git a/src/main/java/org/folio/bulkops/service/BulkOperationServiceHelper.java b/src/main/java/org/folio/bulkops/service/BulkOperationServiceHelper.java
new file mode 100644
index 000000000..bf4a5208a
--- /dev/null
+++ b/src/main/java/org/folio/bulkops/service/BulkOperationServiceHelper.java
@@ -0,0 +1,51 @@
+package org.folio.bulkops.service;
+
+import static java.util.Objects.nonNull;
+import static org.apache.commons.lang3.StringUtils.isEmpty;
+import static org.folio.bulkops.domain.dto.EntityType.INSTANCE_MARC;
+import static org.folio.bulkops.domain.dto.OperationStatusType.COMPLETED;
+import static org.folio.bulkops.domain.dto.OperationStatusType.COMPLETED_WITH_ERRORS;
+import static org.folio.bulkops.domain.dto.OperationStatusType.FAILED;
+import static org.folio.bulkops.util.Constants.ERROR_COMMITTING_FILE_NAME_PREFIX;
+
+import lombok.RequiredArgsConstructor;
+import org.folio.bulkops.domain.entity.BulkOperation;
+import org.folio.bulkops.repository.BulkOperationRepository;
+import org.folio.bulkops.processor.marc.MarcFlowCommitProcessor;
+import org.springframework.stereotype.Service;
+import java.time.LocalDateTime;
+
+@Service
+@RequiredArgsConstructor
+public class BulkOperationServiceHelper {
+  private final BulkOperationRepository bulkOperationRepository;
+  private final ErrorService errorService;
+  private final MarcFlowCommitProcessor marcFlowCommitProcessor;
+  private final LogFilesService logFilesService;
+
+  public void completeBulkOperation(BulkOperation bulkOperation) {
+    bulkOperation.setTotalNumOfRecords(bulkOperation.getMatchedNumOfRecords());
+    bulkOperation.setLinkToCommittedRecordsErrorsCsvFile(errorService.uploadErrorsToStorage(bulkOperation.getId(), ERROR_COMMITTING_FILE_NAME_PREFIX, null));
+    if (nonNull(bulkOperation.getLinkToCommittedRecordsErrorsCsvFile())) {
+      bulkOperation.setCommittedNumOfErrors(errorService.getCommittedNumOfErrors(bulkOperation.getId()));
+      bulkOperation.setCommittedNumOfWarnings(errorService.getCommittedNumOfWarnings(bulkOperation.getId()));
+    }
+    bulkOperation.setStatus(isEmpty(bulkOperation.getLinkToCommittedRecordsErrorsCsvFile()) ? COMPLETED : COMPLETED_WITH_ERRORS);
+    if (INSTANCE_MARC.equals(bulkOperation.getEntityType())) {
+      marcFlowCommitProcessor.processCommit(bulkOperation);
+    }
+    bulkOperation.setProcessedNumOfRecords(bulkOperation.getCommittedNumOfRecords());
+    bulkOperation.setEndTime(LocalDateTime.now());
+    bulkOperationRepository.save(bulkOperation);
+  }
+
+  public void failCommit(BulkOperation bulkOperation, Exception e) {
+    logFilesService.removeCommittedFiles(bulkOperation);
+    bulkOperation.setErrorMessage(e.getMessage());
+    var linkToCommittingErrorsFile = errorService.uploadErrorsToStorage(bulkOperation.getId(), ERROR_COMMITTING_FILE_NAME_PREFIX, bulkOperation.getErrorMessage());
+    bulkOperation.setLinkToCommittedRecordsErrorsCsvFile(linkToCommittingErrorsFile);
+    bulkOperation.setStatus(FAILED);
+    bulkOperation.setEndTime(LocalDateTime.now());
+    bulkOperationRepository.save(bulkOperation);
+  }
+}
diff --git a/src/main/java/org/folio/bulkops/service/LogFilesService.java b/src/main/java/org/folio/bulkops/service/LogFilesService.java
index aac2572eb..db52f5ef0 100644
--- a/src/main/java/org/folio/bulkops/service/LogFilesService.java
+++ b/src/main/java/org/folio/bulkops/service/LogFilesService.java
@@ -37,14 +37,14 @@ public void clearLogFiles() {
     log.info("Found {} old bulk operations to clear files.", oldOperations.size());
     oldOperations.forEach(bulkOperation -> {
       bulkOperation.setExpired(true);
-      removeFiles(bulkOperation);
+      removeCommittedFiles(bulkOperation);
       bulkOperationRepository.save(bulkOperation);
       log.info("Bulk operation with id {} is older than {} days. All files were removed.",
         bulkOperation.getId(), MIN_DAYS_TO_CLEAR_LOG_FILES);
     });
   }
 
-  private void removeFiles(BulkOperation bulkOperation) {
+  public void removeCommittedFiles(BulkOperation bulkOperation) {
     removeTriggeringAndMatchedRecordsFiles(bulkOperation);
     removeModifiedFiles(bulkOperation);
     if (isNotEmpty(bulkOperation.getLinkToCommittedRecordsJsonFile())) {
diff --git a/src/main/java/org/folio/bulkops/service/MarcUpdateService.java b/src/main/java/org/folio/bulkops/service/MarcUpdateService.java
index e5c555dd7..3fdb643ed 100644
--- a/src/main/java/org/folio/bulkops/service/MarcUpdateService.java
+++ b/src/main/java/org/folio/bulkops/service/MarcUpdateService.java
@@ -1,14 +1,12 @@
 package org.folio.bulkops.service;
 
-import static java.util.Objects.nonNull;
 import static org.folio.bulkops.domain.dto.IdentifierType.HRID;
 import static org.folio.bulkops.domain.dto.OperationStatusType.APPLY_MARC_CHANGES;
-import static org.folio.bulkops.domain.dto.OperationStatusType.FAILED;
+import static org.folio.bulkops.util.Constants.CHANGED_MARC_PATH_TEMPLATE;
 import static org.folio.bulkops.util.Constants.ERROR_COMMITTING_FILE_NAME_PREFIX;
 import static org.folio.bulkops.util.Constants.MSG_NO_MARC_CHANGE_REQUIRED;
 import static org.folio.bulkops.util.MarcHelper.fetchInstanceUuidOrElseHrid;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.log4j.Log4j2;
 import org.apache.commons.io.FilenameUtils;
@@ -35,15 +33,12 @@
 @Log4j2
 @RequiredArgsConstructor
 public class MarcUpdateService {
-  public static final String CHANGED_MARC_PATH_TEMPLATE = "%s/%s-Changed-Records-MARC-%s.mrc";
-  public static final String CHANGED_MARC_CSV_PATH_TEMPLATE = "%s/%s-Changed-Records-MARC-CSV-%s.csv";
-
   private final BulkOperationExecutionRepository executionRepository;
   private final RemoteFileSystemClient remoteFileSystemClient;
   private final MarcInstanceUpdateProcessor updateProcessor;
   private final ErrorService errorService;
   private final BulkOperationRepository bulkOperationRepository;
-  private final ObjectMapper objectMapper;
+  private final BulkOperationServiceHelper bulkOperationServiceHelper;
 
   @Transactional
   public void commitForInstanceMarc(BulkOperation bulkOperation) {
@@ -61,27 +56,15 @@ public void commitForInstanceMarc(BulkOperation bulkOperation) {
       try {
         bulkOperation.setLinkToCommittedRecordsMarcFile(prepareCommittedFile(bulkOperation));
         updateProcessor.updateMarcRecords(bulkOperation);
-        var path = bulkOperation.getLinkToCommittedRecordsMarcFile();
-        if (nonNull(path)) {
-          remoteFileSystemClient.remove(path);
-          bulkOperation.setLinkToCommittedRecordsMarcFile(null);
-        }
-        bulkOperationRepository.save(bulkOperation);
         execution = execution
           .withStatus(StatusType.COMPLETED)
           .withEndTime(LocalDateTime.now());
-      } catch (Exception e) {
+      } catch (IOException e) {
         log.error("Error while updating marc file", e);
         execution = execution
           .withStatus(StatusType.FAILED)
           .withEndTime(LocalDateTime.now());
-        bulkOperation.setStatus(FAILED);
-        bulkOperation.setEndTime(LocalDateTime.now());
-        bulkOperation.setErrorMessage(e.getMessage());
-        var linkToCommittingErrorsFile = errorService.uploadErrorsToStorage(bulkOperation.getId(), ERROR_COMMITTING_FILE_NAME_PREFIX, bulkOperation.getErrorMessage());
-        bulkOperation.setLinkToCommittedRecordsErrorsCsvFile(linkToCommittingErrorsFile);
-        bulkOperation.setLinkToCommittedRecordsMarcFile(null);
-        bulkOperationRepository.save(bulkOperation);
+        bulkOperationServiceHelper.failCommit(bulkOperation, e);
       }
       executionRepository.save(execution);
     } else {
diff --git a/src/main/java/org/folio/bulkops/service/SrsService.java b/src/main/java/org/folio/bulkops/service/SrsService.java
index 8ae3ae534..8aaf76f23 100644
--- a/src/main/java/org/folio/bulkops/service/SrsService.java
+++ b/src/main/java/org/folio/bulkops/service/SrsService.java
@@ -1,11 +1,9 @@
 package org.folio.bulkops.service;
 
 import static com.opencsv.ICSVWriter.DEFAULT_SEPARATOR;
-import static org.folio.bulkops.domain.dto.OperationStatusType.COMPLETED;
-import static org.folio.bulkops.domain.dto.OperationStatusType.COMPLETED_WITH_ERRORS;
-import static org.folio.bulkops.service.MarcUpdateService.CHANGED_MARC_CSV_PATH_TEMPLATE;
-import static org.folio.bulkops.service.MarcUpdateService.CHANGED_MARC_PATH_TEMPLATE;
-import static org.folio.bulkops.util.Constants.ERROR_COMMITTING_FILE_NAME_PREFIX;
+import static java.util.Objects.nonNull;
+import static org.folio.bulkops.util.Constants.CHANGED_MARC_CSV_PATH_TEMPLATE;
+import static org.folio.bulkops.util.Constants.CHANGED_MARC_PATH_TEMPLATE;
 
 import com.opencsv.CSVWriterBuilder;
 import lombok.RequiredArgsConstructor;
@@ -16,9 +14,7 @@
 import org.folio.bulkops.domain.bean.GetParsedRecordsBatchConditions;
 import org.folio.bulkops.domain.bean.GetParsedRecordsBatchRequestBody;
 import org.folio.bulkops.domain.entity.BulkOperation;
-import org.folio.bulkops.domain.entity.BulkOperationExecution;
 import org.folio.bulkops.exception.NotFoundException;
-import org.folio.bulkops.repository.BulkOperationExecutionRepository;
 import org.folio.bulkops.repository.BulkOperationRepository;
 import org.folio.bulkops.util.MarcCsvHelper;
 import org.marc4j.MarcJsonReader;
@@ -29,7 +25,6 @@
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.time.LocalDate;
-import java.time.LocalDateTime;
 import java.util.List;
 
 @Service
@@ -41,9 +36,8 @@ public class SrsService {
   private final SrsClient srsClient;
   private final BulkOperationRepository bulkOperationRepository;
   private final RemoteFileSystemClient remoteFileSystemClient;
-  private final ErrorService errorService;
-  private final BulkOperationExecutionRepository executionRepository;
   private final MarcCsvHelper marcCsvHelper;
+  private final BulkOperationServiceHelper bulkOperationServiceHelper;
 
   public void retrieveMarcInstancesFromSrs(List<String> instanceIds, BulkOperation bulkOperation) {
     log.info("Retrieving {} marc instances from SRS", instanceIds.size());
@@ -53,6 +47,11 @@ public void retrieveMarcInstancesFromSrs(List<String> instanceIds, BulkOperation
     var committedRecordsMarcFile = String.format(CHANGED_MARC_PATH_TEMPLATE, bulkOperation.getId(), LocalDate.now(), triggeringFileName);
     var committedRecordsMarcCsvFile = String.format(CHANGED_MARC_CSV_PATH_TEMPLATE, bulkOperation.getId(), LocalDate.now(), triggeringFileName);
     if (!instanceIds.isEmpty()) {
+      var path = bulkOperation.getLinkToCommittedRecordsMarcFile();
+      if (nonNull(path)) {
+        remoteFileSystemClient.remove(path);
+        bulkOperation.setLinkToCommittedRecordsMarcFile(null);
+      }
       try (var writer = remoteFileSystemClient.marcWriter(committedRecordsMarcFile);
            var csvWriter = new CSVWriterBuilder(remoteFileSystemClient.writer(committedRecordsMarcCsvFile))
              .withSeparator(DEFAULT_SEPARATOR).build();) {
@@ -80,7 +79,9 @@ public void retrieveMarcInstancesFromSrs(List<String> instanceIds, BulkOperation
       noLinkToCommitted = true;
     }
     committedRecordsMarcFile = noLinkToCommitted ? null : committedRecordsMarcFile;
-    completeBulkOperation(bulkOperation, fetchedNumOfRecords, committedRecordsMarcFile, committedRecordsMarcCsvFile);
+    bulkOperation.setLinkToCommittedRecordsMarcFile(committedRecordsMarcFile);
+    bulkOperation.setLinkToCommittedRecordsMarcCsvFile(committedRecordsMarcCsvFile);
+    bulkOperationServiceHelper.completeBulkOperation(bulkOperation);
   }
 
   private Record jsonToMarcRecord(String json) throws IOException {
@@ -96,29 +97,4 @@ private void updateBulkOperationProgress(BulkOperation bulkOperation, int proces
     operation.setProcessedNumOfRecords(operation.getProcessedNumOfRecords() + processedNumOfRecords);
     bulkOperationRepository.save(operation);
   }
-
-  private void completeBulkOperation(BulkOperation bulkOperation, int committedNumOfRecords, String committedRecordsMarcFile, String committedRecordsMarcCsvFile) {
-    var operation = bulkOperationRepository.findById(bulkOperation.getId())
-      .orElseThrow(() -> new NotFoundException("BulkOperation was not found by id=" + bulkOperation.getId()));
-    operation.setLinkToCommittedRecordsMarcFile(committedRecordsMarcFile);
-    operation.setLinkToCommittedRecordsMarcCsvFile(committedRecordsMarcCsvFile);
-    operation.setTotalNumOfRecords(operation.getMatchedNumOfRecords());
-    operation.setProcessedNumOfRecords(operation.getMatchedNumOfRecords());
-    operation.setCommittedNumOfRecords(Math.max(committedNumOfRecords, getNumOfProcessedRecords(bulkOperation)));
-    operation.setLinkToCommittedRecordsErrorsCsvFile(errorService.uploadErrorsToStorage(operation.getId(), ERROR_COMMITTING_FILE_NAME_PREFIX, null));
-    operation.setCommittedNumOfErrors(errorService.getCommittedNumOfErrors(operation.getId()));
-    operation.setCommittedNumOfWarnings(errorService.getCommittedNumOfWarnings(operation.getId()));
-    operation.setEndTime(LocalDateTime.now());
-    operation.setStatus(operation.getCommittedNumOfErrors() == 0 ? COMPLETED : COMPLETED_WITH_ERRORS);
-    marcCsvHelper.enrichMarcAndCsvCommittedFiles(operation);
-    bulkOperationRepository.save(operation);
-  }
-
-  private int getNumOfProcessedRecords(BulkOperation bulkOperation) {
-    return executionRepository.findAllByBulkOperationId(bulkOperation.getId()).stream()
-      .map(BulkOperationExecution::getProcessedRecords)
-      .mapToInt(v -> v)
-      .max()
-      .orElse(0);
-  }
 }
diff --git a/src/main/java/org/folio/bulkops/util/Constants.java b/src/main/java/org/folio/bulkops/util/Constants.java
index a5de62c3f..2487315ba 100644
--- a/src/main/java/org/folio/bulkops/util/Constants.java
+++ b/src/main/java/org/folio/bulkops/util/Constants.java
@@ -80,4 +80,8 @@ public class Constants {
   public static final String ERROR_COMMITTING_FILE_NAME_PREFIX = "-Committing-changes-Errors-";
   public static final String ERROR_MATCHING_FILE_NAME_PREFIX = "-Matching-Records-Errors-";
   public static final String ERROR_FILE_NAME_ENDING = "-Errors.csv";
+  public static final String ENRICHED_PREFIX = "enriched-";
+  public static final String CHANGED_CSV_PATH_TEMPLATE = "%s/%s-Changed-Records-CSV-%s.csv";
+  public static final String CHANGED_MARC_PATH_TEMPLATE = "%s/%s-Changed-Records-MARC-%s.mrc";
+  public static final String CHANGED_MARC_CSV_PATH_TEMPLATE = "%s/%s-Changed-Records-MARC-CSV-%s.csv";
 }
diff --git a/src/main/java/org/folio/bulkops/util/MarcCsvHelper.java b/src/main/java/org/folio/bulkops/util/MarcCsvHelper.java
index 3479c6805..88eb8eda5 100644
--- a/src/main/java/org/folio/bulkops/util/MarcCsvHelper.java
+++ b/src/main/java/org/folio/bulkops/util/MarcCsvHelper.java
@@ -1,47 +1,28 @@
 package org.folio.bulkops.util;
 
-import static java.nio.charset.StandardCharsets.UTF_8;
-import static java.util.Objects.isNull;
 import static java.util.Objects.nonNull;
-import static org.apache.commons.io.FilenameUtils.getBaseName;
 import static org.folio.bulkops.domain.bean.Instance.INSTANCE_HRID;
-import static org.folio.bulkops.service.BulkOperationService.CHANGED_CSV_PATH_TEMPLATE;
-import static org.folio.bulkops.service.MarcUpdateService.CHANGED_MARC_PATH_TEMPLATE;
 import static org.folio.bulkops.util.Constants.INSTANCE_NOTE_POSITION;
 import static org.folio.bulkops.util.Constants.ITEM_DELIMITER_SPACED;
-import static org.folio.bulkops.util.Constants.LINE_BREAK;
 
-import com.fasterxml.jackson.core.JsonFactory;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.opencsv.CSVReaderBuilder;
-import com.opencsv.exceptions.CsvValidationException;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.log4j.Log4j2;
-import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.folio.bulkops.client.RemoteFileSystemClient;
-import org.folio.bulkops.domain.bean.ExtendedInstance;
 import org.folio.bulkops.domain.bean.Instance;
-import org.folio.bulkops.domain.converter.BulkOperationsEntityCsvWriter;
 import org.folio.bulkops.domain.dto.Cell;
 import org.folio.bulkops.domain.entity.BulkOperation;
 import org.folio.bulkops.service.Marc21ReferenceProvider;
 import org.folio.bulkops.service.MarcToUnifiedTableRowMapper;
 import org.folio.bulkops.service.NoteTableUpdater;
 import org.folio.bulkops.service.RuleService;
-import org.marc4j.MarcStreamReader;
-import org.marc4j.MarcStreamWriter;
 import org.marc4j.marc.Record;
 import org.springframework.stereotype.Component;
-import java.io.BufferedInputStream;
 import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.io.InputStreamReader;
-import java.io.SequenceInputStream;
 import java.io.StringWriter;
-import java.time.LocalDate;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -52,7 +33,7 @@
 @RequiredArgsConstructor
 @Log4j2
 public class MarcCsvHelper {
-  public static final String ENRICHED_PREFIX = "enriched-";
+
 
   private final NoteTableUpdater noteTableUpdater;
   private final MarcToUnifiedTableRowMapper marcToUnifiedTableRowMapper;
@@ -161,117 +142,4 @@ private void handleSpecialCharacters(String[] strings) {
       strings[i] = s;
     }
   }
-
-  public void enrichMarcAndCsvCommittedFiles(BulkOperation bulkOperation) {
-    var csvHrids = getUpdatedInventoryInstanceHrids(bulkOperation);
-    var marcHrids = getUpdatedMarcInstanceHrids(bulkOperation);
-    enrichCommittedCsvWithUpdatedMarcRecords(bulkOperation, csvHrids, marcHrids);
-    enrichCommittedMarcWithUpdatedInventoryRecords(bulkOperation, csvHrids, marcHrids);
-  }
-
-  public void enrichCommittedCsvWithUpdatedMarcRecords(BulkOperation bulkOperation, List<String> csvHrids, List<String> marcHrids) {
-    var hrids = CollectionUtils.subtract(marcHrids, csvHrids);
-    if (!hrids.isEmpty() && nonNull(bulkOperation.getLinkToMatchedRecordsJsonFile())) {
-      var dirName = isNull(bulkOperation.getLinkToCommittedRecordsCsvFile()) ? bulkOperation.getId() : ENRICHED_PREFIX + bulkOperation.getId();
-      var committedCsvFileName = CHANGED_CSV_PATH_TEMPLATE.formatted(dirName, LocalDate.now(), getBaseName(bulkOperation.getLinkToTriggeringCsvFile()));
-      try (var matchedFileReader = new InputStreamReader(new BufferedInputStream(remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsJsonFile())));
-           var writer = isNull(bulkOperation.getLinkToCommittedRecordsCsvFile()) ?
-             remoteFileSystemClient.writer(committedCsvFileName) : new StringWriter()) {
-        var matchedFileParser = new JsonFactory().createParser(matchedFileReader);
-        var matchedFileIterator = objectMapper.readValues(matchedFileParser, ExtendedInstance.class);
-        var csvWriter = new BulkOperationsEntityCsvWriter(writer, Instance.class);
-        while (matchedFileIterator.hasNext()) {
-          var instance = matchedFileIterator.next().getEntity();
-          if (hrids.contains(instance.getHrid())) {
-            csvWriter.write(instance);
-          }
-        }
-        if (nonNull(bulkOperation.getLinkToCommittedRecordsCsvFile())) {
-          var appendedCsvRecords = writer.toString();
-          appendedCsvRecords = appendedCsvRecords.substring(appendedCsvRecords.indexOf(LINE_BREAK) + 1);
-          var appendedCsvStream = new SequenceInputStream(
-            remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsCsvFile()),
-            new ByteArrayInputStream(appendedCsvRecords.getBytes()));
-          remoteFileSystemClient.put(appendedCsvStream, committedCsvFileName);
-        }
-      } catch (Exception e) {
-        log.error("Failed to enrich csv file", e);
-      } finally {
-        if (nonNull(bulkOperation.getLinkToCommittedRecordsCsvFile())) {
-          remoteFileSystemClient.remove(bulkOperation.getLinkToCommittedRecordsCsvFile());
-        }
-        bulkOperation.setLinkToCommittedRecordsCsvFile(committedCsvFileName);
-      }
-    }
-  }
-
-  public void enrichCommittedMarcWithUpdatedInventoryRecords(BulkOperation bulkOperation, List<String> csvHrids, List<String> marcHrids) {
-    var hrids = CollectionUtils.subtract(csvHrids, marcHrids);
-    if (!hrids.isEmpty() && nonNull(bulkOperation.getLinkToMatchedRecordsMarcFile())) {
-      var linkToCommitted = bulkOperation.getLinkToCommittedRecordsMarcFile();
-      var dirName = isNull(linkToCommitted) ? bulkOperation.getId() : ENRICHED_PREFIX + bulkOperation.getId();
-      var committedMarcFileName = CHANGED_MARC_PATH_TEMPLATE.formatted(dirName, LocalDate.now(), getBaseName(bulkOperation.getLinkToTriggeringCsvFile()));
-      try (var matchedMarcInputStream = remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsMarcFile());
-           var committedMarcInputStream = isNull(linkToCommitted) ? null : remoteFileSystemClient.get(linkToCommitted);
-           var marcOutputStream = new ByteArrayOutputStream()) {
-        var marcReader = new MarcStreamReader(matchedMarcInputStream);
-        var streamWriter = new MarcStreamWriter(marcOutputStream, "UTF-8");
-        while (marcReader.hasNext()) {
-          var marcRecord = marcReader.next();
-          if (hrids.contains(marcRecord.getControlNumber())) {
-            streamWriter.write(marcRecord);
-          }
-        }
-        var appendedMarcStream = new ByteArrayInputStream(marcOutputStream.toString(UTF_8).getBytes());
-        var committedMarcStream = isNull(linkToCommitted) ?
-          appendedMarcStream :
-          new SequenceInputStream(committedMarcInputStream, appendedMarcStream);
-        remoteFileSystemClient.put(committedMarcStream, committedMarcFileName);
-      } catch (Exception e) {
-        log.error("Failed to enrich marc file", e);
-      } finally {
-        if (nonNull(bulkOperation.getLinkToCommittedRecordsMarcFile())) {
-          remoteFileSystemClient.remove(bulkOperation.getLinkToCommittedRecordsMarcFile());
-        }
-        bulkOperation.setLinkToCommittedRecordsMarcFile(committedMarcFileName);
-      }
-    }
-  }
-
-  public List<String> getUpdatedInventoryInstanceHrids(BulkOperation bulkOperation) {
-    List<String> updatedHrids = new ArrayList<>();
-    if (nonNull(bulkOperation.getLinkToCommittedRecordsCsvFile())) {
-      var instanceHeaderNames = UnifiedTableHeaderBuilder.getEmptyTableWithHeaders(Instance.class).getHeader().stream()
-        .map(Cell::getValue)
-        .toList();
-      try (var reader = new CSVReaderBuilder(new InputStreamReader(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsCsvFile())))
-        .withCSVParser(CSVHelper.getCsvParser()).withSkipLines(1).build()) {
-        String[] line;
-        while ((line = reader.readNext()) != null) {
-          if (line.length == instanceHeaderNames.size()) {
-            updatedHrids.add(line[instanceHeaderNames.indexOf(INSTANCE_HRID)]);
-          }
-        }
-      } catch (IOException | CsvValidationException e) {
-        log.error("Failed to read csv file", e);
-      }
-    }
-    return updatedHrids;
-  }
-
-  public List<String> getUpdatedMarcInstanceHrids(BulkOperation bulkOperation) {
-    List<String> updatedHrids = new ArrayList<>();
-    if (nonNull(bulkOperation.getLinkToCommittedRecordsMarcFile())) {
-      try (var marcInputStream = remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsMarcFile())) {
-        var marcReader = new MarcStreamReader(marcInputStream);
-        while (marcReader.hasNext()) {
-          var marcRecord = marcReader.next();
-          updatedHrids.add(marcRecord.getControlNumber());
-        }
-      } catch (IOException e) {
-        log.error("Failed to read marc file", e);
-      }
-    }
-    return updatedHrids;
-  }
 }
diff --git a/src/test/java/org/folio/bulkops/processor/marc/MarcFlowCommitProcessorTest.java b/src/test/java/org/folio/bulkops/processor/marc/MarcFlowCommitProcessorTest.java
new file mode 100644
index 000000000..191138a69
--- /dev/null
+++ b/src/test/java/org/folio/bulkops/processor/marc/MarcFlowCommitProcessorTest.java
@@ -0,0 +1,149 @@
+package org.folio.bulkops.processor.marc;
+
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.folio.bulkops.util.Constants.ENRICHED_PREFIX;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import lombok.SneakyThrows;
+import org.folio.bulkops.BaseTest;
+import org.folio.bulkops.client.RemoteFileSystemClient;
+import org.folio.bulkops.domain.entity.BulkOperation;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.io.SequenceInputStream;
+import java.io.StringWriter;
+import java.util.UUID;
+
+class MarcFlowCommitProcessorTest extends BaseTest {
+  @MockitoBean
+  private RemoteFileSystemClient remoteFileSystemClient;
+  @Autowired
+  private MarcFlowCommitProcessor marcFlowCommitProcessor;
+
+  @Test
+  @SneakyThrows
+  void shouldEnrichCommittedCsvWithUpdatedMarcRecords() {
+    var pathToCommittedCsv = "somedir/committed.csv";
+    var bulkOperation = BulkOperation.builder()
+      .id(UUID.randomUUID())
+      .linkToTriggeringCsvFile("instances.csv")
+      .linkToCommittedRecordsCsvFile(pathToCommittedCsv)
+      .linkToCommittedRecordsMarcFile("committed.mrc")
+      .linkToMatchedRecordsJsonFile("matched.json")
+      .build();
+
+    when(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsCsvFile()))
+      .thenReturn(new FileInputStream("src/test/resources/files/committed_marc_instance.csv"));
+    when(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsMarcFile()))
+      .thenReturn(new FileInputStream("src/test/resources/files/committed_marc_record.mrc"));
+    when(remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsJsonFile()))
+      .thenReturn(new FileInputStream("src/test/resources/files/matched_marc_instances.json"));
+
+    var csvHrids = marcFlowCommitProcessor.getUpdatedInventoryInstanceHrids(bulkOperation);
+    var marcHrids = marcFlowCommitProcessor.getUpdatedMarcInstanceHrids(bulkOperation);
+    marcFlowCommitProcessor.enrichCommittedCsvWithUpdatedMarcRecords(bulkOperation, csvHrids, marcHrids);
+
+    var contentCaptor = ArgumentCaptor.forClass(InputStream.class);
+    var pathCaptor = ArgumentCaptor.forClass(String.class);
+
+    verify(remoteFileSystemClient).remove(pathToCommittedCsv);
+    verify(remoteFileSystemClient).put(contentCaptor.capture(), pathCaptor.capture());
+    assertThat(contentCaptor.getValue()).isInstanceOf(SequenceInputStream.class);
+    assertThat(pathCaptor.getValue()).startsWith(ENRICHED_PREFIX);
+    assertThat(bulkOperation.getLinkToCommittedRecordsCsvFile()).startsWith(ENRICHED_PREFIX);
+  }
+
+  @Test
+  @SneakyThrows
+  void shouldCreateCommittedCsvWithUpdatedMarcRecordsIfNoLink() {
+    var bulkOperation = BulkOperation.builder()
+      .id(UUID.randomUUID())
+      .linkToTriggeringCsvFile("instances.csv")
+      .linkToCommittedRecordsMarcFile("committed.mrc")
+      .linkToMatchedRecordsJsonFile("matched.json")
+      .build();
+
+    var writer = new StringWriter();
+
+    when(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsMarcFile()))
+      .thenReturn(new FileInputStream("src/test/resources/files/committed_marc_record.mrc"));
+    when(remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsJsonFile()))
+      .thenReturn(new FileInputStream("src/test/resources/files/matched_marc_instances.json"));
+    when(remoteFileSystemClient.writer(anyString()))
+      .thenReturn(writer);
+
+    marcFlowCommitProcessor.enrichCommittedCsvWithUpdatedMarcRecords(bulkOperation, singletonList("hrid1"), singletonList("hrid3"));
+
+    assertThat(bulkOperation.getLinkToCommittedRecordsCsvFile()).isNotNull();
+    var expectedCsvContent = """
+      Instance UUID,Suppress from discovery,Staff suppress,Previously held,Instance HRID,Source,Cataloged date,Instance status term,Mode of issuance,Statistical code,Administrative note,Resource title,Index title,Series statements,Contributors,Edition,Physical description,Resource type,Nature of content,Formats,Languages,Publication frequency,Publication range,Notes
+      043c77e9-3653-43d6-a064-5d99b9e5adb4,,,,hrid3,MARC,,,,,,,,,,,,,,,,,,
+      """;
+    assertThat(writer).hasToString(expectedCsvContent);
+  }
+
+  @Test
+  @SneakyThrows
+  void shouldEnrichCommittedMarcWithUpdatedInventoryRecords() {
+    var pathToCommittedMarc = "somedir/committed.mrc";
+    var bulkOperation = BulkOperation.builder()
+      .id(UUID.randomUUID())
+      .linkToTriggeringCsvFile("instances.csv")
+      .linkToCommittedRecordsCsvFile("committed.csv")
+      .linkToCommittedRecordsMarcFile(pathToCommittedMarc)
+      .linkToMatchedRecordsMarcFile("matched.json")
+      .build();
+
+    when(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsCsvFile()))
+      .thenReturn(new FileInputStream("src/test/resources/files/committed_marc_instance.csv"));
+    when(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsMarcFile()))
+      .thenReturn(new FileInputStream("src/test/resources/files/committed_marc_record.mrc"));
+    when(remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsMarcFile()))
+      .thenReturn(new FileInputStream("src/test/resources/files/matched_marc_records.mrc"));
+
+    marcFlowCommitProcessor.enrichCommittedMarcWithUpdatedInventoryRecords(bulkOperation, singletonList("hrid1"), singletonList("hrid3"));
+
+    var contentCaptor = ArgumentCaptor.forClass(InputStream.class);
+    var pathCaptor = ArgumentCaptor.forClass(String.class);
+
+    verify(remoteFileSystemClient).remove(pathToCommittedMarc);
+    verify(remoteFileSystemClient).put(contentCaptor.capture(), pathCaptor.capture());
+    assertThat(contentCaptor.getValue()).isInstanceOf(SequenceInputStream.class);
+    assertThat(pathCaptor.getValue()).startsWith(ENRICHED_PREFIX);
+    assertThat(bulkOperation.getLinkToCommittedRecordsMarcFile()).startsWith(ENRICHED_PREFIX);
+  }
+
+  @Test
+  @SneakyThrows
+  void shouldCreateCommittedMarcWithUpdatedInventoryRecordsIfNoLink() {
+    var bulkOperation = BulkOperation.builder()
+      .id(UUID.randomUUID())
+      .linkToTriggeringCsvFile("instances.csv")
+      .linkToCommittedRecordsCsvFile("committed.csv")
+      .linkToMatchedRecordsMarcFile("matched.json")
+      .build();
+
+    when(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsCsvFile()))
+      .thenReturn(new FileInputStream("src/test/resources/files/committed_marc_instance.csv"));
+    when(remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsMarcFile()))
+      .thenReturn(new FileInputStream("src/test/resources/files/matched_marc_records.mrc"));
+
+    var csvHrids = marcFlowCommitProcessor.getUpdatedInventoryInstanceHrids(bulkOperation);
+    var marcHrids = marcFlowCommitProcessor.getUpdatedMarcInstanceHrids(bulkOperation);
+    marcFlowCommitProcessor.enrichCommittedMarcWithUpdatedInventoryRecords(bulkOperation, csvHrids, marcHrids);
+
+    assertThat(bulkOperation.getLinkToCommittedRecordsMarcFile()).isNotNull();
+
+    var expectedMarcContent = "00044nam a2200037Ia 4500001000600000\u001Ehrid1\u001E\u001D";
+    var contentCaptor = ArgumentCaptor.forClass(InputStream.class);
+    verify(remoteFileSystemClient).put(contentCaptor.capture(), anyString());
+    assertThat(new String(contentCaptor.getValue().readAllBytes())).isEqualTo(expectedMarcContent);
+  }
+}
diff --git a/src/test/java/org/folio/bulkops/processor/marc/MarcInstanceUpdateProcessorTest.java b/src/test/java/org/folio/bulkops/processor/marc/MarcInstanceUpdateProcessorTest.java
index 9bedea63c..d101dd859 100644
--- a/src/test/java/org/folio/bulkops/processor/marc/MarcInstanceUpdateProcessorTest.java
+++ b/src/test/java/org/folio/bulkops/processor/marc/MarcInstanceUpdateProcessorTest.java
@@ -2,8 +2,6 @@
 
 import static org.apache.commons.lang3.StringUtils.EMPTY;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.folio.bulkops.domain.dto.OperationStatusType.COMPLETED;
-import static org.folio.bulkops.domain.dto.OperationStatusType.COMPLETED_WITH_ERRORS;
 import static org.folio.bulkops.util.Constants.ERROR_COMMITTING_FILE_NAME_PREFIX;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
@@ -156,8 +154,5 @@ void shouldNotUploadEmptyFile(int numOfErrors) {
 
     verify(bulkOperationRepository).save(bulkOperationCaptor.capture());
     assertThat(bulkOperationCaptor.getValue().getLinkToCommittedRecordsMarcFile()).isNull();
-
-    var expectedStatus = numOfErrors == 0 ? COMPLETED : COMPLETED_WITH_ERRORS;
-    assertThat(bulkOperationCaptor.getValue().getStatus()).isEqualTo(expectedStatus);
   }
 }
diff --git a/src/test/java/org/folio/bulkops/service/BulkOperationServiceTest.java b/src/test/java/org/folio/bulkops/service/BulkOperationServiceTest.java
index 757e82818..e933b72f3 100644
--- a/src/test/java/org/folio/bulkops/service/BulkOperationServiceTest.java
+++ b/src/test/java/org/folio/bulkops/service/BulkOperationServiceTest.java
@@ -329,7 +329,8 @@ void shouldPopulateErrorToBulkOperationIfS3IssuesForUploadIdentifiersManual() {
   @SneakyThrows
   void shouldThrowExceptionIfOperationIdIsNull() {
     var file = new MockMultipartFile("file", "barcodes.csv", MediaType.TEXT_PLAIN_VALUE, new FileInputStream("src/test/resources/files/modified-user.csv").readAllBytes());
-    assertThrows(NotFoundException.class, () -> bulkOperationService.uploadCsvFile(USER, IdentifierType.BARCODE, true, null, UUID.randomUUID(), file));
+    var bulkOperationId = UUID.randomUUID();
+    assertThrows(NotFoundException.class, () -> bulkOperationService.uploadCsvFile(USER, IdentifierType.BARCODE, true, null, bulkOperationId, file));
   }
 
   @ParameterizedTest
@@ -1365,7 +1366,7 @@ void shouldUpdateStatusesWhenCommitChangesFails() {
     assertThat(updatedExecution.getStatus(), equalTo(StatusType.FAILED));
 
     var operationCaptor = ArgumentCaptor.forClass(BulkOperation.class);
-    verify(bulkOperationRepository, times(2)).save(operationCaptor.capture());
+    verify(bulkOperationRepository, times(3)).save(operationCaptor.capture());
     var secondCapture = operationCaptor.getAllValues().get(1);
     assertThat(secondCapture.getStatus(), equalTo(OperationStatusType.FAILED));
     assertThat(secondCapture.getEndTime(), notNullValue());
@@ -1813,7 +1814,7 @@ void shouldSkipCommitIfNoRules() {
 
     var operationCaptor = ArgumentCaptor.forClass(BulkOperation.class);
     verify(bulkOperationRepository, times(2)).save(operationCaptor.capture());
-    var lastCapture = operationCaptor.getAllValues().get(1);
+    var lastCapture = operationCaptor.getValue();
     assertNull(lastCapture.getLinkToCommittedRecordsMarcFile());
     assertNull(lastCapture.getLinkToCommittedRecordsCsvFile());
   }
@@ -1823,13 +1824,14 @@ void shouldSkipCommitIfNoRules() {
     true  | false
     false | true
     """, delimiter = '|')
-  void shouldStartCommitInstanceMarcIfRulesArePresent(boolean hasAdministrativeRules, boolean hasMarcRules) {
+  void shouldStartCommitInstanceMarcIfModifiedMarcFileIsPresent(boolean hasAdministrativeRules, boolean hasMarcRules) {
     var operationId = UUID.randomUUID();
     var operation = BulkOperation.builder()
       .id(operationId)
       .entityType(INSTANCE_MARC)
       .linkToTriggeringCsvFile("instances.csv")
       .linkToModifiedRecordsJsonFile("modified.json")
+      .linkToModifiedRecordsMarcFile(hasMarcRules ? "modified.mrc" : null)
       .build();
 
     when(executionRepository.save(any(BulkOperationExecution.class)))
diff --git a/src/test/java/org/folio/bulkops/service/MarcUpdateServiceTest.java b/src/test/java/org/folio/bulkops/service/MarcUpdateServiceTest.java
index dce1c0cb9..a608d3457 100644
--- a/src/test/java/org/folio/bulkops/service/MarcUpdateServiceTest.java
+++ b/src/test/java/org/folio/bulkops/service/MarcUpdateServiceTest.java
@@ -2,7 +2,7 @@
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.folio.bulkops.domain.dto.OperationStatusType.FAILED;
-import static org.folio.bulkops.service.MarcUpdateService.CHANGED_MARC_PATH_TEMPLATE;
+import static org.folio.bulkops.util.Constants.CHANGED_MARC_PATH_TEMPLATE;
 import static org.folio.bulkops.util.Constants.MSG_NO_MARC_CHANGE_REQUIRED;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -31,6 +31,7 @@
 import org.springframework.test.context.bean.override.mockito.MockitoBean;
 
 import java.io.FileInputStream;
+import java.io.IOException;
 import java.time.LocalDate;
 import java.util.UUID;
 
@@ -86,9 +87,6 @@ void shouldCommitInstanceMarc() {
 
     marcUpdateService.commitForInstanceMarc(bulkOperation);
 
-    verify(updateProcessor).updateMarcRecords(bulkOperationArgumentCaptor.capture());
-    assertThat(bulkOperationArgumentCaptor.getValue().getLinkToCommittedRecordsMarcFile()).isNull();
-
     verify(executionRepository, times(2)).save(executionArgumentCaptor.capture());
     assertThat(executionArgumentCaptor.getAllValues().get(1).getStatus()).isEqualTo(StatusType.COMPLETED);
 
@@ -133,7 +131,7 @@ void shouldCommitInstanceMarcWhenNoChangesRequired() {
 
   @Test
   @SneakyThrows
-  void shouldFailOperationInCaseOfException() {
+  void shouldFailOperationInCaseOfIOException() {
     var bulkOperation = BulkOperation.builder()
       .id(UUID.randomUUID())
       .linkToTriggeringCsvFile("triggering.csv")
@@ -146,7 +144,7 @@ void shouldFailOperationInCaseOfException() {
     var pathToCommittedMarcFile = String.format(CHANGED_MARC_PATH_TEMPLATE, bulkOperation.getId(), LocalDate.now(), "triggering");
 
     var mockMarcWriter = mock(MarcRemoteStorageWriter.class);
-    doThrow(new IllegalArgumentException())
+    doThrow(new IOException())
       .when(mockMarcWriter).writeRecord(any(Record.class));
     when(executionRepository.save(any(BulkOperationExecution.class)))
       .thenReturn(execution);
@@ -162,7 +160,7 @@ void shouldFailOperationInCaseOfException() {
     verify(executionRepository, times(2)).save(executionArgumentCaptor.capture());
     assertThat(executionArgumentCaptor.getAllValues().get(1).getStatus()).isEqualTo(StatusType.FAILED);
 
-    verify(bulkOperationRepository, times(2)).save(bulkOperationArgumentCaptor.capture());
+    verify(bulkOperationRepository, times(3)).save(bulkOperationArgumentCaptor.capture());
     assertThat(bulkOperationArgumentCaptor.getValue().getStatus()).isEqualTo(FAILED);
   }
 }
diff --git a/src/test/java/org/folio/bulkops/service/SrsServiceTest.java b/src/test/java/org/folio/bulkops/service/SrsServiceTest.java
index 7dfbf93ee..8c5a46b80 100644
--- a/src/test/java/org/folio/bulkops/service/SrsServiceTest.java
+++ b/src/test/java/org/folio/bulkops/service/SrsServiceTest.java
@@ -2,7 +2,6 @@
 
 import static org.apache.commons.lang3.StringUtils.EMPTY;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.folio.bulkops.domain.dto.OperationStatusType.COMPLETED;
 import static org.folio.bulkops.domain.dto.OperationStatusType.COMPLETED_WITH_ERRORS;
 import static org.folio.bulkops.util.Constants.ERROR_COMMITTING_FILE_NAME_PREFIX;
 import static org.mockito.ArgumentMatchers.any;
@@ -96,9 +95,8 @@ void shouldRetrieveRecordsFromSrsAndCompleteBulkOperation(int numOfErrors) {
     verify(marcWriter).writeRecord(any(Record.class));
     verify(bulkOperationRepository, times(2))
       .save(bulkOperationCaptor.capture());
-    var expectedStatus = numOfErrors == 0 ? COMPLETED : COMPLETED_WITH_ERRORS;
     assertThat(bulkOperationCaptor.getAllValues().get(1).getStatus())
-      .isEqualTo(expectedStatus);
+      .isEqualTo(COMPLETED_WITH_ERRORS);
   }
 
   @SneakyThrows
diff --git a/src/test/java/org/folio/bulkops/util/MarcCsvHelperTest.java b/src/test/java/org/folio/bulkops/util/MarcCsvHelperTest.java
index 55fac5702..2297a9b3f 100644
--- a/src/test/java/org/folio/bulkops/util/MarcCsvHelperTest.java
+++ b/src/test/java/org/folio/bulkops/util/MarcCsvHelperTest.java
@@ -4,9 +4,6 @@
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.folio.bulkops.domain.dto.OperationStatusType.APPLY_CHANGES;
 import static org.folio.bulkops.domain.dto.OperationStatusType.REVIEW_CHANGES;
-import static org.folio.bulkops.util.MarcCsvHelper.ENRICHED_PREFIX;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import lombok.SneakyThrows;
@@ -28,14 +25,10 @@
 import org.marc4j.marc.impl.LeaderImpl;
 import org.marc4j.marc.impl.RecordImpl;
 import org.marc4j.marc.impl.SubfieldImpl;
-import org.mockito.ArgumentCaptor;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.test.context.bean.override.mockito.MockitoBean;
 
 import java.io.FileInputStream;
-import java.io.InputStream;
-import java.io.SequenceInputStream;
-import java.io.StringWriter;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.UUID;
@@ -172,124 +165,4 @@ void shouldNotEnrichCsvIfMarcCsvIsInvalid() {
 
     assertThat(new String(res)).contains(expectedInstanceNotes);
   }
-
-  @Test
-  @SneakyThrows
-  void shouldEnrichCommittedCsvWithUpdatedMarcRecords() {
-    var pathToCommittedCsv = "somedir/committed.csv";
-    var bulkOperation = BulkOperation.builder()
-      .id(UUID.randomUUID())
-      .linkToTriggeringCsvFile("instances.csv")
-      .linkToCommittedRecordsCsvFile(pathToCommittedCsv)
-      .linkToCommittedRecordsMarcFile("committed.mrc")
-      .linkToMatchedRecordsJsonFile("matched.json")
-      .build();
-
-    when(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsCsvFile()))
-      .thenReturn(new FileInputStream("src/test/resources/files/committed_marc_instance.csv"));
-    when(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsMarcFile()))
-      .thenReturn(new FileInputStream("src/test/resources/files/committed_marc_record.mrc"));
-    when(remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsJsonFile()))
-    .thenReturn(new FileInputStream("src/test/resources/files/matched_marc_instances.json"));
-
-    var csvHrids = marcCsvHelper.getUpdatedInventoryInstanceHrids(bulkOperation);
-    var marcHrids = marcCsvHelper.getUpdatedMarcInstanceHrids(bulkOperation);
-    marcCsvHelper.enrichCommittedCsvWithUpdatedMarcRecords(bulkOperation, csvHrids, marcHrids);
-
-    var contentCaptor = ArgumentCaptor.forClass(InputStream.class);
-    var pathCaptor = ArgumentCaptor.forClass(String.class);
-
-    verify(remoteFileSystemClient).remove(pathToCommittedCsv);
-    verify(remoteFileSystemClient).put(contentCaptor.capture(), pathCaptor.capture());
-    assertThat(contentCaptor.getValue()).isInstanceOf(SequenceInputStream.class);
-    assertThat(pathCaptor.getValue()).startsWith(ENRICHED_PREFIX);
-    assertThat(bulkOperation.getLinkToCommittedRecordsCsvFile()).startsWith(ENRICHED_PREFIX);
-  }
-
-  @Test
-  @SneakyThrows
-  void shouldCreateCommittedCsvWithUpdatedMarcRecordsIfNoLink() {
-    var bulkOperation = BulkOperation.builder()
-      .id(UUID.randomUUID())
-      .linkToTriggeringCsvFile("instances.csv")
-      .linkToCommittedRecordsMarcFile("committed.mrc")
-      .linkToMatchedRecordsJsonFile("matched.json")
-      .build();
-
-    var writer = new StringWriter();
-
-    when(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsMarcFile()))
-      .thenReturn(new FileInputStream("src/test/resources/files/committed_marc_record.mrc"));
-    when(remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsJsonFile()))
-      .thenReturn(new FileInputStream("src/test/resources/files/matched_marc_instances.json"));
-    when(remoteFileSystemClient.writer(anyString()))
-      .thenReturn(writer);
-
-    marcCsvHelper.enrichCommittedCsvWithUpdatedMarcRecords(bulkOperation, singletonList("hrid1"), singletonList("hrid3"));
-
-    assertThat(bulkOperation.getLinkToCommittedRecordsCsvFile()).isNotNull();
-    var expectedCsvContent = """
-      Instance UUID,Suppress from discovery,Staff suppress,Previously held,Instance HRID,Source,Cataloged date,Instance status term,Mode of issuance,Statistical code,Administrative note,Resource title,Index title,Series statements,Contributors,Edition,Physical description,Resource type,Nature of content,Formats,Languages,Publication frequency,Publication range,Notes
-      043c77e9-3653-43d6-a064-5d99b9e5adb4,,,,hrid3,MARC,,,,,,,,,,,,,,,,,,
-      """;
-    assertThat(writer).hasToString(expectedCsvContent);
-  }
-
-  @Test
-  @SneakyThrows
-  void shouldEnrichCommittedMarcWithUpdatedInventoryRecords() {
-    var pathToCommittedMarc = "somedir/committed.mrc";
-    var bulkOperation = BulkOperation.builder()
-      .id(UUID.randomUUID())
-      .linkToTriggeringCsvFile("instances.csv")
-      .linkToCommittedRecordsCsvFile("committed.csv")
-      .linkToCommittedRecordsMarcFile(pathToCommittedMarc)
-      .linkToMatchedRecordsMarcFile("matched.json")
-      .build();
-
-    when(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsCsvFile()))
-      .thenReturn(new FileInputStream("src/test/resources/files/committed_marc_instance.csv"));
-    when(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsMarcFile()))
-      .thenReturn(new FileInputStream("src/test/resources/files/committed_marc_record.mrc"));
-    when(remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsMarcFile()))
-      .thenReturn(new FileInputStream("src/test/resources/files/matched_marc_records.mrc"));
-
-    marcCsvHelper.enrichCommittedMarcWithUpdatedInventoryRecords(bulkOperation, singletonList("hrid1"), singletonList("hrid3"));
-
-    var contentCaptor = ArgumentCaptor.forClass(InputStream.class);
-    var pathCaptor = ArgumentCaptor.forClass(String.class);
-
-    verify(remoteFileSystemClient).remove(pathToCommittedMarc);
-    verify(remoteFileSystemClient).put(contentCaptor.capture(), pathCaptor.capture());
-    assertThat(contentCaptor.getValue()).isInstanceOf(SequenceInputStream.class);
-    assertThat(pathCaptor.getValue()).startsWith(ENRICHED_PREFIX);
-    assertThat(bulkOperation.getLinkToCommittedRecordsMarcFile()).startsWith(ENRICHED_PREFIX);
-  }
-
-  @Test
-  @SneakyThrows
-  void shouldCreateCommittedMarcWithUpdatedInventoryRecordsIfNoLink() {
-    var bulkOperation = BulkOperation.builder()
-      .id(UUID.randomUUID())
-      .linkToTriggeringCsvFile("instances.csv")
-      .linkToCommittedRecordsCsvFile("committed.csv")
-      .linkToMatchedRecordsMarcFile("matched.json")
-      .build();
-
-    when(remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsCsvFile()))
-      .thenReturn(new FileInputStream("src/test/resources/files/committed_marc_instance.csv"));
-    when(remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsMarcFile()))
-      .thenReturn(new FileInputStream("src/test/resources/files/matched_marc_records.mrc"));
-
-    var csvHrids = marcCsvHelper.getUpdatedInventoryInstanceHrids(bulkOperation);
-    var marcHrids = marcCsvHelper.getUpdatedMarcInstanceHrids(bulkOperation);
-    marcCsvHelper.enrichCommittedMarcWithUpdatedInventoryRecords(bulkOperation, csvHrids, marcHrids);
-
-    assertThat(bulkOperation.getLinkToCommittedRecordsMarcFile()).isNotNull();
-
-    var expectedMarcContent = "00044nam a2200037Ia 4500001000600000\u001Ehrid1\u001E\u001D";
-    var contentCaptor = ArgumentCaptor.forClass(InputStream.class);
-    verify(remoteFileSystemClient).put(contentCaptor.capture(), anyString());
-    assertThat(new String(contentCaptor.getValue().readAllBytes())).isEqualTo(expectedMarcContent);
-  }
 }


Reviews:
user: login: khandramai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T11:54:24Z
user: login: khandramai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T12:01:16Z
user: login: khandramai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T12:03:52Z
user: login: khandramai
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T12:04:57Z
user: login: obozhko-folio
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-17T12:00:18Z

Review comments:
diff_hunk: @@ -0,0 +1,179 @@
+package org.folio.bulkops.processor.marc;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.isNull;
+import static java.util.Objects.nonNull;
+import static org.apache.commons.io.FilenameUtils.getBaseName;
+import static org.folio.bulkops.domain.bean.Instance.INSTANCE_HRID;
+import static org.folio.bulkops.util.Constants.CHANGED_CSV_PATH_TEMPLATE;
+import static org.folio.bulkops.util.Constants.CHANGED_MARC_PATH_TEMPLATE;
+import static org.folio.bulkops.util.Constants.ENRICHED_PREFIX;
+import static org.folio.bulkops.util.Constants.LINE_BREAK;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.opencsv.CSVReaderBuilder;
+import com.opencsv.exceptions.CsvDataTypeMismatchException;
+import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
+import com.opencsv.exceptions.CsvValidationException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.apache.commons.collections4.CollectionUtils;
+import org.folio.bulkops.client.RemoteFileSystemClient;
+import org.folio.bulkops.domain.bean.BulkOperationsEntity;
+import org.folio.bulkops.domain.bean.ExtendedInstance;
+import org.folio.bulkops.domain.bean.Instance;
+import org.folio.bulkops.domain.converter.BulkOperationsEntityCsvWriter;
+import org.folio.bulkops.domain.entity.BulkOperation;
+import org.folio.bulkops.exception.ConverterException;
+import org.folio.bulkops.util.CSVHelper;
+import org.folio.bulkops.util.UnifiedTableHeaderBuilder;
+import org.marc4j.MarcStreamReader;
+import org.marc4j.MarcStreamWriter;
+import org.springframework.stereotype.Service;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.SequenceInputStream;
+import java.io.StringWriter;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+@Log4j2
+public class MarcFlowCommitProcessor {
path: src/main/java/org/folio/bulkops/processor/marc/MarcFlowCommitProcessor.java
user: login: khandramai
body: This should implement interface. New or existing interface should be implemented/reused.
created_at: 2025-03-14T11:54:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+package org.folio.bulkops.processor.marc;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.isNull;
+import static java.util.Objects.nonNull;
+import static org.apache.commons.io.FilenameUtils.getBaseName;
+import static org.folio.bulkops.domain.bean.Instance.INSTANCE_HRID;
+import static org.folio.bulkops.util.Constants.CHANGED_CSV_PATH_TEMPLATE;
+import static org.folio.bulkops.util.Constants.CHANGED_MARC_PATH_TEMPLATE;
+import static org.folio.bulkops.util.Constants.ENRICHED_PREFIX;
+import static org.folio.bulkops.util.Constants.LINE_BREAK;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.opencsv.CSVReaderBuilder;
+import com.opencsv.exceptions.CsvDataTypeMismatchException;
+import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
+import com.opencsv.exceptions.CsvValidationException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.apache.commons.collections4.CollectionUtils;
+import org.folio.bulkops.client.RemoteFileSystemClient;
+import org.folio.bulkops.domain.bean.BulkOperationsEntity;
+import org.folio.bulkops.domain.bean.ExtendedInstance;
+import org.folio.bulkops.domain.bean.Instance;
+import org.folio.bulkops.domain.converter.BulkOperationsEntityCsvWriter;
+import org.folio.bulkops.domain.entity.BulkOperation;
+import org.folio.bulkops.exception.ConverterException;
+import org.folio.bulkops.util.CSVHelper;
+import org.folio.bulkops.util.UnifiedTableHeaderBuilder;
+import org.marc4j.MarcStreamReader;
+import org.marc4j.MarcStreamWriter;
+import org.springframework.stereotype.Service;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.SequenceInputStream;
+import java.io.StringWriter;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+@Log4j2
+public class MarcFlowCommitProcessor {
+  private final RemoteFileSystemClient remoteFileSystemClient;
+  private final ObjectMapper objectMapper;
+
+  public void processMarcFlowCommitResult(BulkOperation bulkOperation) {
+    var csvHrids = getUpdatedInventoryInstanceHrids(bulkOperation);
+    var marcHrids = getUpdatedMarcInstanceHrids(bulkOperation);
+
+    var updatedHrids = new HashSet<>(csvHrids);
+    updatedHrids.addAll(marcHrids);
+    bulkOperation.setCommittedNumOfRecords(updatedHrids.size());
+
+    enrichCommittedCsvWithUpdatedMarcRecords(bulkOperation, csvHrids, marcHrids);
+    enrichCommittedMarcWithUpdatedInventoryRecords(bulkOperation, csvHrids, marcHrids);
+  }
+
+  public void enrichCommittedCsvWithUpdatedMarcRecords(BulkOperation bulkOperation, List<String> csvHrids, List<String> marcHrids) {
+    var hrids = CollectionUtils.subtract(marcHrids, csvHrids);
+    if (!hrids.isEmpty() && nonNull(bulkOperation.getLinkToMatchedRecordsJsonFile())) {
+      var dirName = isNull(bulkOperation.getLinkToCommittedRecordsCsvFile()) ? bulkOperation.getId().toString() : ENRICHED_PREFIX + bulkOperation.getId();
+      var committedCsvFileName = CHANGED_CSV_PATH_TEMPLATE.formatted(dirName, LocalDate.now(), getBaseName(bulkOperation.getLinkToTriggeringCsvFile()));
+      try (var matchedFileReader = new InputStreamReader(new BufferedInputStream(remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsJsonFile())));
+           var writer = isNull(bulkOperation.getLinkToCommittedRecordsCsvFile()) ?
+             remoteFileSystemClient.writer(committedCsvFileName) : new StringWriter()) {
+        var matchedFileParser = new JsonFactory().createParser(matchedFileReader);
+        var matchedFileIterator = objectMapper.readValues(matchedFileParser, ExtendedInstance.class);
+        var csvWriter = new BulkOperationsEntityCsvWriter(writer, Instance.class);
+        while (matchedFileIterator.hasNext()) {
+          var instance = matchedFileIterator.next().getEntity();
+          if (hrids.contains(instance.getHrid())) {
+            writeBeanToCsv(csvWriter, instance);
+          }
+        }
+        if (nonNull(bulkOperation.getLinkToCommittedRecordsCsvFile())) {
+          var appendedCsvRecords = writer.toString();
+          appendedCsvRecords = appendedCsvRecords.substring(appendedCsvRecords.indexOf(LINE_BREAK) + 1);
+          var appendedCsvStream = new SequenceInputStream(
+            remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsCsvFile()),
+            new ByteArrayInputStream(appendedCsvRecords.getBytes()));
+          remoteFileSystemClient.put(appendedCsvStream, committedCsvFileName);
+        }
+      } catch (Exception e) {
+        log.error("Failed to enrich csv file", e);
path: src/main/java/org/folio/bulkops/processor/marc/MarcFlowCommitProcessor.java
user: login: khandramai
body: It makes sense to rethrow exception to stop process and avoid results inconsistency? Other than that looks reasonable to handle S3 issues separately to cover S3-specific issues. 
created_at: 2025-03-14T12:01:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+package org.folio.bulkops.processor.marc;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.isNull;
+import static java.util.Objects.nonNull;
+import static org.apache.commons.io.FilenameUtils.getBaseName;
+import static org.folio.bulkops.domain.bean.Instance.INSTANCE_HRID;
+import static org.folio.bulkops.util.Constants.CHANGED_CSV_PATH_TEMPLATE;
+import static org.folio.bulkops.util.Constants.CHANGED_MARC_PATH_TEMPLATE;
+import static org.folio.bulkops.util.Constants.ENRICHED_PREFIX;
+import static org.folio.bulkops.util.Constants.LINE_BREAK;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.opencsv.CSVReaderBuilder;
+import com.opencsv.exceptions.CsvDataTypeMismatchException;
+import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
+import com.opencsv.exceptions.CsvValidationException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.apache.commons.collections4.CollectionUtils;
+import org.folio.bulkops.client.RemoteFileSystemClient;
+import org.folio.bulkops.domain.bean.BulkOperationsEntity;
+import org.folio.bulkops.domain.bean.ExtendedInstance;
+import org.folio.bulkops.domain.bean.Instance;
+import org.folio.bulkops.domain.converter.BulkOperationsEntityCsvWriter;
+import org.folio.bulkops.domain.entity.BulkOperation;
+import org.folio.bulkops.exception.ConverterException;
+import org.folio.bulkops.util.CSVHelper;
+import org.folio.bulkops.util.UnifiedTableHeaderBuilder;
+import org.marc4j.MarcStreamReader;
+import org.marc4j.MarcStreamWriter;
+import org.springframework.stereotype.Service;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.SequenceInputStream;
+import java.io.StringWriter;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+@Log4j2
+public class MarcFlowCommitProcessor {
+  private final RemoteFileSystemClient remoteFileSystemClient;
+  private final ObjectMapper objectMapper;
+
+  public void processMarcFlowCommitResult(BulkOperation bulkOperation) {
+    var csvHrids = getUpdatedInventoryInstanceHrids(bulkOperation);
+    var marcHrids = getUpdatedMarcInstanceHrids(bulkOperation);
+
+    var updatedHrids = new HashSet<>(csvHrids);
+    updatedHrids.addAll(marcHrids);
+    bulkOperation.setCommittedNumOfRecords(updatedHrids.size());
+
+    enrichCommittedCsvWithUpdatedMarcRecords(bulkOperation, csvHrids, marcHrids);
+    enrichCommittedMarcWithUpdatedInventoryRecords(bulkOperation, csvHrids, marcHrids);
+  }
+
+  public void enrichCommittedCsvWithUpdatedMarcRecords(BulkOperation bulkOperation, List<String> csvHrids, List<String> marcHrids) {
+    var hrids = CollectionUtils.subtract(marcHrids, csvHrids);
+    if (!hrids.isEmpty() && nonNull(bulkOperation.getLinkToMatchedRecordsJsonFile())) {
+      var dirName = isNull(bulkOperation.getLinkToCommittedRecordsCsvFile()) ? bulkOperation.getId().toString() : ENRICHED_PREFIX + bulkOperation.getId();
+      var committedCsvFileName = CHANGED_CSV_PATH_TEMPLATE.formatted(dirName, LocalDate.now(), getBaseName(bulkOperation.getLinkToTriggeringCsvFile()));
+      try (var matchedFileReader = new InputStreamReader(new BufferedInputStream(remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsJsonFile())));
+           var writer = isNull(bulkOperation.getLinkToCommittedRecordsCsvFile()) ?
+             remoteFileSystemClient.writer(committedCsvFileName) : new StringWriter()) {
+        var matchedFileParser = new JsonFactory().createParser(matchedFileReader);
+        var matchedFileIterator = objectMapper.readValues(matchedFileParser, ExtendedInstance.class);
+        var csvWriter = new BulkOperationsEntityCsvWriter(writer, Instance.class);
+        while (matchedFileIterator.hasNext()) {
+          var instance = matchedFileIterator.next().getEntity();
+          if (hrids.contains(instance.getHrid())) {
+            writeBeanToCsv(csvWriter, instance);
+          }
+        }
+        if (nonNull(bulkOperation.getLinkToCommittedRecordsCsvFile())) {
+          var appendedCsvRecords = writer.toString();
+          appendedCsvRecords = appendedCsvRecords.substring(appendedCsvRecords.indexOf(LINE_BREAK) + 1);
+          var appendedCsvStream = new SequenceInputStream(
+            remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsCsvFile()),
+            new ByteArrayInputStream(appendedCsvRecords.getBytes()));
+          remoteFileSystemClient.put(appendedCsvStream, committedCsvFileName);
+        }
+      } catch (Exception e) {
+        log.error("Failed to enrich csv file", e);
+      } finally {
+        if (nonNull(bulkOperation.getLinkToCommittedRecordsCsvFile())) {
path: src/main/java/org/folio/bulkops/processor/marc/MarcFlowCommitProcessor.java
user: login: khandramai
body: This can be moved from finally to try section. This will allow to handle potential S3 issues in the same manner.
created_at: 2025-03-14T12:03:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,179 @@
+package org.folio.bulkops.processor.marc;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Objects.isNull;
+import static java.util.Objects.nonNull;
+import static org.apache.commons.io.FilenameUtils.getBaseName;
+import static org.folio.bulkops.domain.bean.Instance.INSTANCE_HRID;
+import static org.folio.bulkops.util.Constants.CHANGED_CSV_PATH_TEMPLATE;
+import static org.folio.bulkops.util.Constants.CHANGED_MARC_PATH_TEMPLATE;
+import static org.folio.bulkops.util.Constants.ENRICHED_PREFIX;
+import static org.folio.bulkops.util.Constants.LINE_BREAK;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.opencsv.CSVReaderBuilder;
+import com.opencsv.exceptions.CsvDataTypeMismatchException;
+import com.opencsv.exceptions.CsvRequiredFieldEmptyException;
+import com.opencsv.exceptions.CsvValidationException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
+import org.apache.commons.collections4.CollectionUtils;
+import org.folio.bulkops.client.RemoteFileSystemClient;
+import org.folio.bulkops.domain.bean.BulkOperationsEntity;
+import org.folio.bulkops.domain.bean.ExtendedInstance;
+import org.folio.bulkops.domain.bean.Instance;
+import org.folio.bulkops.domain.converter.BulkOperationsEntityCsvWriter;
+import org.folio.bulkops.domain.entity.BulkOperation;
+import org.folio.bulkops.exception.ConverterException;
+import org.folio.bulkops.util.CSVHelper;
+import org.folio.bulkops.util.UnifiedTableHeaderBuilder;
+import org.marc4j.MarcStreamReader;
+import org.marc4j.MarcStreamWriter;
+import org.springframework.stereotype.Service;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.SequenceInputStream;
+import java.io.StringWriter;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+@Service
+@RequiredArgsConstructor
+@Log4j2
+public class MarcFlowCommitProcessor {
+  private final RemoteFileSystemClient remoteFileSystemClient;
+  private final ObjectMapper objectMapper;
+
+  public void processMarcFlowCommitResult(BulkOperation bulkOperation) {
+    var csvHrids = getUpdatedInventoryInstanceHrids(bulkOperation);
+    var marcHrids = getUpdatedMarcInstanceHrids(bulkOperation);
+
+    var updatedHrids = new HashSet<>(csvHrids);
+    updatedHrids.addAll(marcHrids);
+    bulkOperation.setCommittedNumOfRecords(updatedHrids.size());
+
+    enrichCommittedCsvWithUpdatedMarcRecords(bulkOperation, csvHrids, marcHrids);
+    enrichCommittedMarcWithUpdatedInventoryRecords(bulkOperation, csvHrids, marcHrids);
+  }
+
+  public void enrichCommittedCsvWithUpdatedMarcRecords(BulkOperation bulkOperation, List<String> csvHrids, List<String> marcHrids) {
+    var hrids = CollectionUtils.subtract(marcHrids, csvHrids);
+    if (!hrids.isEmpty() && nonNull(bulkOperation.getLinkToMatchedRecordsJsonFile())) {
+      var dirName = isNull(bulkOperation.getLinkToCommittedRecordsCsvFile()) ? bulkOperation.getId().toString() : ENRICHED_PREFIX + bulkOperation.getId();
+      var committedCsvFileName = CHANGED_CSV_PATH_TEMPLATE.formatted(dirName, LocalDate.now(), getBaseName(bulkOperation.getLinkToTriggeringCsvFile()));
+      try (var matchedFileReader = new InputStreamReader(new BufferedInputStream(remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsJsonFile())));
+           var writer = isNull(bulkOperation.getLinkToCommittedRecordsCsvFile()) ?
+             remoteFileSystemClient.writer(committedCsvFileName) : new StringWriter()) {
+        var matchedFileParser = new JsonFactory().createParser(matchedFileReader);
+        var matchedFileIterator = objectMapper.readValues(matchedFileParser, ExtendedInstance.class);
+        var csvWriter = new BulkOperationsEntityCsvWriter(writer, Instance.class);
+        while (matchedFileIterator.hasNext()) {
+          var instance = matchedFileIterator.next().getEntity();
+          if (hrids.contains(instance.getHrid())) {
+            writeBeanToCsv(csvWriter, instance);
+          }
+        }
+        if (nonNull(bulkOperation.getLinkToCommittedRecordsCsvFile())) {
+          var appendedCsvRecords = writer.toString();
+          appendedCsvRecords = appendedCsvRecords.substring(appendedCsvRecords.indexOf(LINE_BREAK) + 1);
+          var appendedCsvStream = new SequenceInputStream(
+            remoteFileSystemClient.get(bulkOperation.getLinkToCommittedRecordsCsvFile()),
+            new ByteArrayInputStream(appendedCsvRecords.getBytes()));
+          remoteFileSystemClient.put(appendedCsvStream, committedCsvFileName);
+        }
+      } catch (Exception e) {
+        log.error("Failed to enrich csv file", e);
+      } finally {
+        if (nonNull(bulkOperation.getLinkToCommittedRecordsCsvFile())) {
+          remoteFileSystemClient.remove(bulkOperation.getLinkToCommittedRecordsCsvFile());
+        }
+        bulkOperation.setLinkToCommittedRecordsCsvFile(committedCsvFileName);
+      }
+    }
+  }
+
+  private void writeBeanToCsv(BulkOperationsEntityCsvWriter writer, BulkOperationsEntity bean)
+    throws CsvRequiredFieldEmptyException, CsvDataTypeMismatchException {
+    try {
+      writer.write(bean);
+    } catch (ConverterException e) {
+      writeBeanToCsv(writer, bean);
+    }
+  }
+
+  public void enrichCommittedMarcWithUpdatedInventoryRecords(BulkOperation bulkOperation, List<String> csvHrids, List<String> marcHrids) {
+    var hrids = CollectionUtils.subtract(csvHrids, marcHrids);
+    if (!hrids.isEmpty() && nonNull(bulkOperation.getLinkToMatchedRecordsMarcFile())) {
+      var linkToCommitted = bulkOperation.getLinkToCommittedRecordsMarcFile();
+      var dirName = isNull(linkToCommitted) ? bulkOperation.getId().toString() : ENRICHED_PREFIX + bulkOperation.getId();
+      var committedMarcFileName = CHANGED_MARC_PATH_TEMPLATE.formatted(dirName, LocalDate.now(), getBaseName(bulkOperation.getLinkToTriggeringCsvFile()));
+      try (var matchedMarcInputStream = remoteFileSystemClient.get(bulkOperation.getLinkToMatchedRecordsMarcFile());
+           var committedMarcInputStream = isNull(linkToCommitted) ? null : remoteFileSystemClient.get(linkToCommitted);
+           var marcOutputStream = new ByteArrayOutputStream()) {
+        var marcReader = new MarcStreamReader(matchedMarcInputStream);
+        var streamWriter = new MarcStreamWriter(marcOutputStream, "UTF-8");
+        while (marcReader.hasNext()) {
+          var marcRecord = marcReader.next();
+          if (hrids.contains(marcRecord.getControlNumber())) {
+            streamWriter.write(marcRecord);
+          }
+        }
+        var appendedMarcStream = new ByteArrayInputStream(marcOutputStream.toString(UTF_8).getBytes());
+        var committedMarcStream = isNull(linkToCommitted) ?
+          appendedMarcStream :
+          new SequenceInputStream(committedMarcInputStream, appendedMarcStream);
+        remoteFileSystemClient.put(committedMarcStream, committedMarcFileName);
+      } catch (Exception e) {
path: src/main/java/org/folio/bulkops/processor/marc/MarcFlowCommitProcessor.java
user: login: khandramai
body: The same with exceptions handling and processing S3 exceptions like for FOLIO cases.
created_at: 2025-03-14T12:04:57Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-17T11:59:15Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=org.folio%3Amod-bulk-operations&pullRequest=389) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=org.folio%3Amod-bulk-operations&pullRequest=389&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=org.folio%3Amod-bulk-operations&pullRequest=389&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=org.folio%3Amod-bulk-operations&pullRequest=389&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [92.3% Coverage on New Code](https://sonarcloud.io/component_measures?id=org.folio%3Amod-bulk-operations&pullRequest=389&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=org.folio%3Amod-bulk-operations&pullRequest=389&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=org.folio%3Amod-bulk-operations&pullRequest=389)


author_association: NONE