FINERACT-1972 Custom Snapshot Event Triggered by COB
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractProfiles.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractProfiles.java
index 6d07570..97aca96 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractProfiles.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractProfiles.java
@@ -23,5 +23,7 @@
     public static final String LIQUIBASE_ONLY = "liquibase-only";
     public static final String DIAGNOSTICS = "diagnostics";
 
+    public static final String TEST = "test";
+
     private FineractProfiles() {}
 }
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/api/InternalExternalEventsApiResource.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/api/InternalExternalEventsApiResource.java
new file mode 100644
index 0000000..31b84f9
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/api/InternalExternalEventsApiResource.java
@@ -0,0 +1,80 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.event.external.api;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.MediaType;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
+import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
+import org.apache.fineract.infrastructure.event.external.service.InternalExternalEventService;
+import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+
+@Profile(FineractProfiles.TEST)
+@Component
+@Path("/v1/internal/externalevents")
+@RequiredArgsConstructor
+@Slf4j
+public class InternalExternalEventsApiResource implements InitializingBean {
+
+    private final InternalExternalEventService internalExternalEventService;
+    private final DefaultToApiJsonSerializer<List<ExternalEventDTO>> jsonSerializer;
+
+    @Override
+    @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
+    public void afterPropertiesSet() throws Exception {
+        log.warn("------------------------------------------------------------");
+        log.warn("                                                            ");
+        log.warn("DO NOT USE THIS IN PRODUCTION!");
+        log.warn("Internal client services mode is enabled");
+        log.warn("DO NOT USE THIS IN PRODUCTION!");
+        log.warn("                                                            ");
+        log.warn("------------------------------------------------------------");
+    }
+
+    @GET
+    @Consumes({ MediaType.APPLICATION_JSON })
+    @Produces({ MediaType.APPLICATION_JSON })
+    public String getAllExternalEvents(@QueryParam("idempotencyKey") final String idempotencyKey, @QueryParam("type") final String type,
+            @QueryParam("category") final String category, @QueryParam("aggregateRootId") final Long aggregateRootId) {
+        log.debug("getAllExternalEvents called with params idempotencyKey:{}, type:{}, category:{}, aggregateRootId:{}  ", idempotencyKey,
+                type, category, aggregateRootId);
+        List<ExternalEventDTO> allExternalEvents = internalExternalEventService.getAllExternalEvents(idempotencyKey, type, category,
+                aggregateRootId);
+        return jsonSerializer.serialize(allExternalEvents);
+    }
+
+    @DELETE
+    public void deleteAllExternalEvents() {
+        log.debug("deleteAllExternalEvents called");
+        internalExternalEventService.deleteAllExternalEvents();
+    }
+
+}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/repository/ExternalEventRepository.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/repository/ExternalEventRepository.java
index 474384d..9f7ccbd 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/repository/ExternalEventRepository.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/repository/ExternalEventRepository.java
@@ -26,11 +26,12 @@
 import org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEventView;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 import org.springframework.data.jpa.repository.Modifying;
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.query.Param;
 
-public interface ExternalEventRepository extends JpaRepository<ExternalEvent, Long> {
+public interface ExternalEventRepository extends JpaRepository<ExternalEvent, Long>, JpaSpecificationExecutor<ExternalEvent> {
 
     List<ExternalEventView> findByStatusOrderById(ExternalEventStatus status, Pageable batchSize);
 
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/InternalExternalEventService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/InternalExternalEventService.java
new file mode 100644
index 0000000..b044b3d
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/InternalExternalEventService.java
@@ -0,0 +1,147 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.event.external.service;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.avro.BulkMessageItemV1;
+import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
+import org.apache.fineract.infrastructure.event.external.repository.ExternalEventRepository;
+import org.apache.fineract.infrastructure.event.external.repository.domain.ExternalEvent;
+import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
+import org.springframework.context.annotation.Profile;
+import org.springframework.data.jpa.domain.Specification;
+import org.springframework.stereotype.Service;
+
+@Service
+@Profile(FineractProfiles.TEST)
+@Slf4j
+@AllArgsConstructor
+public class InternalExternalEventService {
+
+    private final ExternalEventRepository externalEventRepository;
+
+    public void deleteAllExternalEvents() {
+        externalEventRepository.deleteAll();
+    }
+
+    public List<ExternalEventDTO> getAllExternalEvents(String idempotencyKey, String type, String category, Long aggregateRootId) {
+        List<Specification<ExternalEvent>> specifications = new ArrayList<>();
+
+        if (StringUtils.isNotEmpty(idempotencyKey)) {
+            specifications.add(hasIdempotencyKey(idempotencyKey));
+        }
+
+        if (StringUtils.isNotEmpty(type)) {
+            specifications.add(hasType(type));
+        }
+
+        if (StringUtils.isNotEmpty(category)) {
+            specifications.add(hasCategory(category));
+        }
+
+        if (aggregateRootId != null) {
+            specifications.add(hasAggregateRootId(aggregateRootId));
+        }
+
+        Specification<ExternalEvent> reducedSpecification = specifications.stream().reduce(Specification::and)
+                .orElse((Specification<ExternalEvent>) (root, query, criteriaBuilder) -> null);
+        List<ExternalEvent> externalEvents = externalEventRepository.findAll(reducedSpecification);
+
+        try {
+            return convertToReadableFormat(externalEvents);
+        } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException
+                | JsonProcessingException e) {
+            throw new RuntimeException("Error while converting external events to readable format", e);
+        }
+    }
+
+    private Specification<ExternalEvent> hasIdempotencyKey(String idempotencyKey) {
+        return (root, query, cb) -> cb.equal(root.get("idempotencyKey"), idempotencyKey);
+    }
+
+    private Specification<ExternalEvent> hasType(String type) {
+        return (root, query, cb) -> cb.equal(root.get("type"), type);
+    }
+
+    private Specification<ExternalEvent> hasCategory(String category) {
+        return (root, query, cb) -> cb.equal(root.get("category"), category);
+    }
+
+    private Specification<ExternalEvent> hasAggregateRootId(Long aggregateRootId) {
+        return (root, query, cb) -> cb.equal(root.get("aggregateRootId"), aggregateRootId);
+    }
+
+    private List<ExternalEventDTO> convertToReadableFormat(List<ExternalEvent> externalEvents) throws ClassNotFoundException,
+            NoSuchMethodException, InvocationTargetException, IllegalAccessException, JsonProcessingException {
+        List<ExternalEventDTO> eventMessages = new ArrayList<>();
+        for (ExternalEvent externalEvent : externalEvents) {
+            Class<?> payLoadClass = Class.forName(externalEvent.getSchema());
+            ByteBuffer byteBuffer = ByteBuffer.wrap(externalEvent.getData());
+            Method method = payLoadClass.getMethod("fromByteBuffer", ByteBuffer.class);
+            Object payLoad = method.invoke(null, byteBuffer);
+            if (externalEvent.getType().equalsIgnoreCase("BulkBusinessEvent")) {
+                Method methodToGetDatas = payLoad.getClass().getMethod("getDatas", (Class<?>) null);
+                List<BulkMessageItemV1> bulkMessages = (List<BulkMessageItemV1>) methodToGetDatas.invoke(payLoad);
+                StringBuilder bulkMessagePayload = new StringBuilder();
+                for (BulkMessageItemV1 bulkMessage : bulkMessages) {
+                    ExternalEventDTO bulkMessageData = retrieveBulkMessage(bulkMessage, externalEvent);
+                    bulkMessagePayload.append(bulkMessageData);
+                    bulkMessagePayload.append(System.lineSeparator());
+                }
+                eventMessages.add(new ExternalEventDTO(externalEvent.getId(), externalEvent.getType(), externalEvent.getCategory(),
+                        externalEvent.getCreatedAt(), toJsonMap(bulkMessagePayload.toString()), externalEvent.getBusinessDate(),
+                        externalEvent.getSchema(), externalEvent.getAggregateRootId()));
+
+            } else {
+                eventMessages.add(new ExternalEventDTO(externalEvent.getId(), externalEvent.getType(), externalEvent.getCategory(),
+                        externalEvent.getCreatedAt(), toJsonMap(payLoad.toString()), externalEvent.getBusinessDate(),
+                        externalEvent.getSchema(), externalEvent.getAggregateRootId()));
+            }
+        }
+
+        return eventMessages;
+    }
+
+    private ExternalEventDTO retrieveBulkMessage(BulkMessageItemV1 messageItem, ExternalEvent externalEvent) throws ClassNotFoundException,
+            InvocationTargetException, IllegalAccessException, NoSuchMethodException, JsonProcessingException {
+        Class<?> messageBulkMessagePayLoad = Class.forName(messageItem.getDataschema());
+        Method methodForPayLoad = messageBulkMessagePayLoad.getMethod("fromByteBuffer", ByteBuffer.class);
+        Object payLoadBulkItem = methodForPayLoad.invoke(null, messageItem.getData());
+        return new ExternalEventDTO((long) messageItem.getId(), messageItem.getType(), messageItem.getCategory(),
+                externalEvent.getCreatedAt(), toJsonMap(payLoadBulkItem.toString()), externalEvent.getBusinessDate(),
+                externalEvent.getSchema(), externalEvent.getAggregateRootId());
+    }
+
+    private Map<String, Object> toJsonMap(String json) throws JsonProcessingException {
+        ObjectMapper objectMapper = new ObjectMapper();
+        return objectMapper.readValue(json, new TypeReference<>() {});
+    }
+
+}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/validation/ExternalEventDTO.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/validation/ExternalEventDTO.java
new file mode 100644
index 0000000..dec7037
--- /dev/null
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/validation/ExternalEventDTO.java
@@ -0,0 +1,41 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.event.external.service.validation;
+
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@AllArgsConstructor
+@ToString
+public class ExternalEventDTO {
+
+    private final Long eventId;
+    private final String type;
+    private final String category;
+    private final OffsetDateTime createdAt;
+    private final Map<String, Object> payLoad;
+    private final LocalDate businessDate;
+    private final String schema;
+    private final Long aggregateRootId;
+}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanAccountCustomSnapshotBusinessEvent.java b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanAccountCustomSnapshotBusinessEvent.java
new file mode 100644
index 0000000..6284296
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanAccountCustomSnapshotBusinessEvent.java
@@ -0,0 +1,35 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.infrastructure.event.business.domain.loan;
+
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+
+public class LoanAccountCustomSnapshotBusinessEvent extends LoanBusinessEvent {
+
+    private static final String TYPE = "LoanAccountCustomSnapshotBusinessEvent";
+
+    public LoanAccountCustomSnapshotBusinessEvent(Loan value) {
+        super(value);
+    }
+
+    @Override
+    public String getType() {
+        return TYPE;
+    }
+}
diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml
index b08ead7..4d5fb3f 100644
--- a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml
+++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/module-changelog-master.xml
@@ -36,4 +36,5 @@
   <include relativeToChangelogFile="true" file="parts/1011_add_delinquency_actions_table.xml"/>
   <include relativeToChangelogFile="true" file="parts/1012_introduce_loan_schedule_processing_type_configuration.xml"/>
   <include relativeToChangelogFile="true" file="parts/1013_add_loan_account_delinquency_pause_changed_event.xml"/>
+  <include relativeToChangelogFile="true" file="parts/1014_add_loan_account_custom_snapshot_event.xml"/>
 </databaseChangeLog>
diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1014_add_loan_account_custom_snapshot_event.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1014_add_loan_account_custom_snapshot_event.xml
new file mode 100644
index 0000000..4c1ad18
--- /dev/null
+++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1014_add_loan_account_custom_snapshot_event.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements. See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership. The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License. You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied. See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+                   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                   xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
+
+    <changeSet author="fineract" id="1">
+        <insert tableName="m_external_event_configuration">
+            <column name="type" value="LoanAccountCustomSnapshotBusinessEvent"/>
+            <column name="enabled" valueBoolean="false"/>
+        </insert>
+    </changeSet>
+
+</databaseChangeLog>
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java
index 7402955..c767a54 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java
@@ -36,6 +36,7 @@
 import org.apache.fineract.cob.loan.RetrieveLoanIdService;
 import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
 import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
+import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
 import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
 import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
 import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
@@ -43,7 +44,7 @@
 import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Component;
 
-@Profile("test")
+@Profile(FineractProfiles.TEST)
 @Component
 @Path("/v1/internal/cob")
 @RequiredArgsConstructor
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalLoanAccountLockApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalLoanAccountLockApiResource.java
index 9d5d731..a925719 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalLoanAccountLockApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalLoanAccountLockApiResource.java
@@ -35,13 +35,14 @@
 import org.apache.fineract.cob.domain.LoanAccountLockRepository;
 import org.apache.fineract.cob.domain.LockOwner;
 import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
 import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
 import org.springframework.beans.factory.InitializingBean;
 import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Component;
 import org.springframework.web.bind.annotation.RequestBody;
 
-@Profile("test")
+@Profile(FineractProfiles.TEST)
 @Component
 @Path("/v1/internal/loans")
 @RequiredArgsConstructor
diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckDueInstallmentsBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckDueInstallmentsBusinessStep.java
new file mode 100644
index 0000000..fcce730
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CheckDueInstallmentsBusinessStep.java
@@ -0,0 +1,69 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.loan;
+
+import java.time.LocalDate;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountCustomSnapshotBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class CheckDueInstallmentsBusinessStep implements LoanCOBBusinessStep {
+
+    private final BusinessEventNotifierService businessEventNotifierService;
+
+    @Override
+    public Loan execute(Loan loan) {
+        log.debug("start processing custom snapshot event trigger business step loan for loan with id [{}]", loan.getId());
+
+        if (loan.getRepaymentScheduleInstallments() != null && loan.getRepaymentScheduleInstallments().size() > 0) {
+            final LocalDate currentDate = DateUtils.getBusinessLocalDate();
+            boolean shouldPostCustomSnapshotBusinessEvent = false;
+            for (int i = 0; i < loan.getRepaymentScheduleInstallments().size(); i++) {
+                if (loan.getRepaymentScheduleInstallments().get(i).getDueDate().equals(currentDate)
+                        && loan.getRepaymentScheduleInstallments().get(i).isNotFullyPaidOff()) {
+                    shouldPostCustomSnapshotBusinessEvent = true;
+                }
+            }
+            if (shouldPostCustomSnapshotBusinessEvent) {
+                businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountCustomSnapshotBusinessEvent(loan));
+            }
+        }
+
+        log.debug("end processing custom snapshot event trigger business step for loan with id [{}]", loan.getId());
+        return loan;
+    }
+
+    @Override
+    public String getEnumStyledName() {
+        return "CHECK_DUE_INSTALLMENTS";
+    }
+
+    @Override
+    public String getHumanReadableName() {
+        return "Check Due Installments";
+    }
+
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/instancemode/api/InstanceModeApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/instancemode/api/InstanceModeApiResource.java
index 215b754..837638f 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/instancemode/api/InstanceModeApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/instancemode/api/InstanceModeApiResource.java
@@ -33,12 +33,13 @@
 import jakarta.ws.rs.core.Response;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
 import org.apache.fineract.infrastructure.core.config.FineractProperties;
 import org.springframework.beans.factory.InitializingBean;
 import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Component;
 
-@Profile("test")
+@Profile(FineractProfiles.TEST)
 @Component
 @Path("/v1/instance-mode")
 @Tag(name = "Instance Mode", description = "Instance mode changing API")
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/s3/LocalstackS3ClientCustomizer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/s3/LocalstackS3ClientCustomizer.java
index 6b764aa..4e7d108 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/s3/LocalstackS3ClientCustomizer.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/s3/LocalstackS3ClientCustomizer.java
@@ -20,6 +20,7 @@
 
 import java.net.URI;
 import lombok.RequiredArgsConstructor;
+import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
 import org.apache.poi.util.StringUtil;
 import org.springframework.context.annotation.Profile;
 import org.springframework.core.env.Environment;
@@ -28,7 +29,7 @@
 
 @Component
 @RequiredArgsConstructor
-@Profile("test")
+@Profile(FineractProfiles.TEST)
 public class LocalstackS3ClientCustomizer implements S3ClientCustomizer {
 
     private final Environment environment;
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/InternalClientInformationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/InternalClientInformationApiResource.java
index 6b1342b..ae454ca 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/InternalClientInformationApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/api/InternalClientInformationApiResource.java
@@ -37,6 +37,7 @@
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
+import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
 import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
 import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
 import org.apache.fineract.portfolio.client.domain.Client;
@@ -45,7 +46,7 @@
 import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Component;
 
-@Profile("test")
+@Profile(FineractProfiles.TEST)
 @Component
 @Path("/v1/internal/client")
 @RequiredArgsConstructor
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java
index 7969fd9..a15d7f4 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java
@@ -38,6 +38,7 @@
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
+import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
 import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
 import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
@@ -50,7 +51,7 @@
 import org.springframework.context.annotation.Profile;
 import org.springframework.stereotype.Component;
 
-@Profile("test")
+@Profile(FineractProfiles.TEST)
 @Component
 @Path("/v1/internal/loan")
 @RequiredArgsConstructor
diff --git a/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckDueInstallmentsBusinessStepTest.java b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckDueInstallmentsBusinessStepTest.java
new file mode 100644
index 0000000..161123c
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/cob/loan/CheckDueInstallmentsBusinessStepTest.java
@@ -0,0 +1,161 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.cob.loan;
+
+import static org.mockito.Mockito.times;
+
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.core.domain.ActionContext;
+import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountCustomSnapshotBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class CheckDueInstallmentsBusinessStepTest {
+
+    @Mock
+    private BusinessEventNotifierService businessEventNotifierService;
+
+    @Captor
+    private ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor;
+
+    @InjectMocks
+    private CheckDueInstallmentsBusinessStep underTest;
+
+    /**
+     * Setup context before each test.
+     */
+    @BeforeEach
+    public void setUp() {
+        ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
+        ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
+        ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, LocalDate.parse("2024-01-16"),
+                BusinessDateType.COB_DATE, LocalDate.parse("2024-01-15"))));
+    }
+
+    @AfterEach
+    public void tearDown() {
+        ThreadLocalContextUtil.reset();
+    }
+
+    @Test
+    public void testNoRepaymentScheduleInLoan() {
+        // given
+        Loan loan = Mockito.mock(Loan.class);
+
+        // when
+        underTest.execute(loan);
+
+        // then
+        Mockito.verifyNoInteractions(businessEventNotifierService);
+    }
+
+    @Test
+    public void testInstallmentDueDateIsNotMatchingWithCurrentBusinessDate() {
+        // given
+        Loan loan = Mockito.mock(Loan.class);
+        Mockito.when(loan.getRepaymentScheduleInstallments()).thenReturn(List.of(Mockito.mock(LoanRepaymentScheduleInstallment.class)));
+        Mockito.when(loan.getRepaymentScheduleInstallments().get(0).getDueDate()).thenReturn(LocalDate.parse("2024-01-17"));
+
+        // when
+        underTest.execute(loan);
+
+        // then
+        Mockito.verifyNoInteractions(businessEventNotifierService);
+    }
+
+    @Test
+    public void testSingleInstallmentDueDateIsMatchingWithCurrentBusinessDateAndNotFullyPayed() {
+        // given
+        Loan loan = Mockito.mock(Loan.class);
+        Mockito.when(loan.getRepaymentScheduleInstallments()).thenReturn(List.of(Mockito.mock(LoanRepaymentScheduleInstallment.class)));
+        Mockito.when(loan.getRepaymentScheduleInstallments().get(0).getDueDate()).thenReturn(LocalDate.parse("2024-01-16"));
+        Mockito.when(loan.getRepaymentScheduleInstallments().get(0).isNotFullyPaidOff()).thenReturn(true);
+
+        // when
+        underTest.execute(loan);
+
+        // then
+        Mockito.verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(businessEventArgumentCaptor.capture());
+        BusinessEvent<?> rawEvent = businessEventArgumentCaptor.getValue();
+        Assertions.assertInstanceOf(LoanAccountCustomSnapshotBusinessEvent.class, rawEvent);
+        LoanAccountCustomSnapshotBusinessEvent event = (LoanAccountCustomSnapshotBusinessEvent) rawEvent;
+        Assertions.assertEquals(loan, event.get());
+    }
+
+    @Test
+    public void testSingleInstallmentDueDateIsMatchingWithCurrentBusinessDateAndFullyPayed() {
+        // given
+        Loan loan = Mockito.mock(Loan.class);
+        Mockito.when(loan.getRepaymentScheduleInstallments()).thenReturn(List.of(Mockito.mock(LoanRepaymentScheduleInstallment.class)));
+        Mockito.when(loan.getRepaymentScheduleInstallments().get(0).getDueDate()).thenReturn(LocalDate.parse("2024-01-16"));
+        Mockito.when(loan.getRepaymentScheduleInstallments().get(0).isNotFullyPaidOff()).thenReturn(false);
+
+        // when
+        underTest.execute(loan);
+
+        // then
+        Mockito.verifyNoInteractions(businessEventNotifierService);
+    }
+
+    @Test
+    public void testMultipleInstallmentDueDateIsMatchingWithCurrentBusinessDateAndNotFullyPayedButSingleEventIsGenerated() {
+        // given
+        Loan loan = Mockito.mock(Loan.class);
+        Mockito.when(loan.getRepaymentScheduleInstallments()).thenReturn(
+                List.of(Mockito.mock(LoanRepaymentScheduleInstallment.class), Mockito.mock(LoanRepaymentScheduleInstallment.class)));
+        // first one is a down payment installment
+        Mockito.when(loan.getRepaymentScheduleInstallments().get(0).getDueDate()).thenReturn(LocalDate.parse("2024-01-16"));
+        Mockito.when(loan.getRepaymentScheduleInstallments().get(0).isNotFullyPaidOff()).thenReturn(true);
+        Mockito.lenient().when(loan.getRepaymentScheduleInstallments().get(0).isDownPayment()).thenReturn(true);
+        // this one is a real installment
+        Mockito.when(loan.getRepaymentScheduleInstallments().get(1).getDueDate()).thenReturn(LocalDate.parse("2024-01-16"));
+        Mockito.when(loan.getRepaymentScheduleInstallments().get(1).isNotFullyPaidOff()).thenReturn(true);
+
+        // when
+        underTest.execute(loan);
+
+        // then
+        Mockito.verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(businessEventArgumentCaptor.capture());
+        BusinessEvent<?> rawEvent = businessEventArgumentCaptor.getValue();
+        Assertions.assertInstanceOf(LoanAccountCustomSnapshotBusinessEvent.class, rawEvent);
+        LoanAccountCustomSnapshotBusinessEvent event = (LoanAccountCustomSnapshotBusinessEvent) rawEvent;
+        Assertions.assertEquals(loan, event.get());
+    }
+
+}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
index ca46595..69b69d9 100644
--- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
+++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
@@ -98,7 +98,7 @@
                 "LoanChargeOffPostBusinessEvent", "LoanUndoChargeOffBusinessEvent", "LoanAccrualTransactionCreatedBusinessEvent",
                 "LoanRescheduledDueAdjustScheduleBusinessEvent", "LoanOwnershipTransferBusinessEvent", "LoanAccountSnapshotBusinessEvent",
                 "LoanTransactionDownPaymentPostBusinessEvent", "LoanTransactionDownPaymentPreBusinessEvent",
-                "LoanAccountDelinquencyPauseChangedBusinessEvent");
+                "LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent");
 
         List<FineractPlatformTenant> tenants = Arrays
                 .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
@@ -178,7 +178,7 @@
                 "LoanUndoChargeOffBusinessEvent", "LoanAccrualTransactionCreatedBusinessEvent",
                 "LoanRescheduledDueAdjustScheduleBusinessEvent", "LoanOwnershipTransferBusinessEvent", "LoanAccountSnapshotBusinessEvent",
                 "LoanTransactionDownPaymentPostBusinessEvent", "LoanTransactionDownPaymentPreBusinessEvent",
-                "LoanAccountDelinquencyPauseChangedBusinessEvent");
+                "LoanAccountDelinquencyPauseChangedBusinessEvent", "LoanAccountCustomSnapshotBusinessEvent");
 
         List<FineractPlatformTenant> tenants = Arrays
                 .asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index e04e414..dcc4c1d 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -74,6 +74,8 @@
 import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
 import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
 import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.extension.ExtendWith;
 
@@ -86,7 +88,7 @@
 
     protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
 
-    protected final ResponseSpecification responseSpec = createResponseSpecification(200);
+    protected final ResponseSpecification responseSpec = createResponseSpecification(Matchers.is(200));
     protected final RequestSpecification requestSpec = createRequestSpecification();
 
     protected final AccountHelper accountHelper = new AccountHelper(requestSpec, responseSpec);
@@ -261,8 +263,8 @@
         return request;
     }
 
-    private static ResponseSpecification createResponseSpecification(int statusCode) {
-        return new ResponseSpecBuilder().expectStatusCode(statusCode).build();
+    protected static ResponseSpecification createResponseSpecification(Matcher<Integer> statusCodeMatcher) {
+        return new ResponseSpecBuilder().expectStatusCode(statusCodeMatcher).build();
     }
 
     protected void verifyUndoLastDisbursalShallFail(Long loanId, String expectedError) {
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
new file mode 100644
index 0000000..d2a29aa
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
@@ -0,0 +1,326 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
+
+import com.google.gson.Gson;
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.client.models.BusinessDateRequest;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
+import org.apache.fineract.integrationtests.common.BusinessStepHelper;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.ExternalEventConfigurationHelper;
+import org.apache.fineract.integrationtests.common.SchedulerJobHelper;
+import org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper;
+import org.apache.fineract.integrationtests.common.externalevents.ExternalEventsExtension;
+import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@Slf4j
+@ExtendWith({ LoanTestLifecycleExtension.class, ExternalEventsExtension.class })
+public class CustomSnapshotEventIntegrationTest extends BaseLoanIntegrationTest {
+
+    private SchedulerJobHelper schedulerJobHelper = new SchedulerJobHelper(this.requestSpec);
+
+    @Test
+    public void testSnapshotEventGenerationWhenLoanInstallmentIsNotPayed() {
+        runAt("31 January 2023", () -> {
+            // Enable Business Step
+            enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
+                    "CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
+                    "EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS");
+
+            enableLoanAccountCustomSnapshotBusinessEvent();
+
+            // Create Client
+            Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            // Create Loan Product
+            PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
+                    InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
+            loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
+            PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
+
+            // Apply and Approve Loan
+            Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "01 January 2023", 1250.0, 4);
+
+            // Disburse Loan
+            disburseLoan(loanId, BigDecimal.valueOf(1250), "01 January 2023");
+
+            // Verify Repayment Schedule and Due Dates
+            verifyRepaymentSchedule(loanId, //
+                    installment(0, null, "01 January 2023"), //
+                    installment(313.0, false, "31 January 2023"), //
+                    installment(313.0, false, "02 March 2023"), //
+                    installment(313.0, false, "01 April 2023"), //
+                    installment(311.0, false, "01 May 2023") //
+            );
+
+            // delete all external events
+            deleteAllExternalEvents();
+
+            // run cob
+            updateBusinessDateAndExecuteCOBJob("01 February 2023");
+
+            // verify external events
+            List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
+            Assertions.assertEquals(1, allExternalEvents.size());
+            Assertions.assertEquals("LoanAccountCustomSnapshotBusinessEvent", allExternalEvents.get(0).getType());
+            Assertions.assertEquals(loanId, allExternalEvents.get(0).getAggregateRootId());
+        });
+    }
+
+    @Test
+    public void testNoSnapshotEventGenerationWhenLoanInstallmentIsPayed() {
+        runAt("31 January 2023", () -> {
+            // Enable Business Step
+            enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
+                    "CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
+                    "EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS");
+
+            enableLoanAccountCustomSnapshotBusinessEvent();
+
+            // Create Client
+            Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            // Create Loan Product
+            PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
+                    InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
+            loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
+            PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
+
+            // Apply and Approve Loan
+            Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "01 January 2023", 1250.0, 4);
+
+            // Disburse Loan
+            disburseLoan(loanId, BigDecimal.valueOf(1250), "01 January 2023");
+
+            // Verify Repayment Schedule and Due Dates
+            verifyRepaymentSchedule(loanId, //
+                    installment(0, null, "01 January 2023"), //
+                    installment(313.0, false, "31 January 2023"), //
+                    installment(313.0, false, "02 March 2023"), //
+                    installment(313.0, false, "01 April 2023"), //
+                    installment(311.0, false, "01 May 2023") //
+            );
+
+            addRepaymentForLoan(loanId, 313.0, "31 January 2023");
+
+            // Verify Repayment Schedule and Due Dates
+            verifyRepaymentSchedule(loanId, //
+                    installment(0, null, "01 January 2023"), //
+                    installment(313.0, true, "31 January 2023"), //
+                    installment(313.0, false, "02 March 2023"), //
+                    installment(313.0, false, "01 April 2023"), //
+                    installment(311.0, false, "01 May 2023") //
+            );
+
+            // delete all external events
+            deleteAllExternalEvents();
+
+            // run cob
+            updateBusinessDateAndExecuteCOBJob("01 February 2023");
+
+            // verify external events
+            List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
+            Assertions.assertEquals(0, allExternalEvents.size());
+        });
+    }
+
+    @Test
+    public void testNoSnapshotEventGenerationWhenWhenCustomSnapshotEventCOBTaskIsNotActive() {
+        runAt("31 January 2023", () -> {
+            // Enable Business Step
+            enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
+                    "CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
+                    "EXTERNAL_ASSET_OWNER_TRANSFER");
+
+            enableLoanAccountCustomSnapshotBusinessEvent();
+
+            // Create Client
+            Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            // Create Loan Product
+            PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
+                    InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
+            loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
+            PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
+
+            // Apply and Approve Loan
+            Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "01 January 2023", 1250.0, 4);
+
+            // Disburse Loan
+            disburseLoan(loanId, BigDecimal.valueOf(1250), "01 January 2023");
+
+            // Verify Repayment Schedule and Due Dates
+            verifyRepaymentSchedule(loanId, //
+                    installment(0, null, "01 January 2023"), //
+                    installment(313.0, false, "31 January 2023"), //
+                    installment(313.0, false, "02 March 2023"), //
+                    installment(313.0, false, "01 April 2023"), //
+                    installment(311.0, false, "01 May 2023") //
+            );
+
+            // delete all external events
+            deleteAllExternalEvents();
+
+            // run cob
+            updateBusinessDateAndExecuteCOBJob("01 February 2023");
+
+            // verify external events
+            List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
+            Assertions.assertEquals(0, allExternalEvents.size());
+        });
+    }
+
+    @Test
+    public void testNoSnapshotEventGenerationWhenCOBDateIsNotMatchingWithInstallmentDueDate() {
+        runAt("30 January 2023", () -> {
+            // Enable Business Step
+            enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
+                    "CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
+                    "EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS");
+
+            enableLoanAccountCustomSnapshotBusinessEvent();
+
+            // Create Client
+            Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            // Create Loan Product
+            PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
+                    InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
+            loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
+            PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
+
+            // Apply and Approve Loan
+            Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "01 January 2023", 1250.0, 4);
+
+            // Disburse Loan
+            disburseLoan(loanId, BigDecimal.valueOf(1250), "01 January 2023");
+
+            // Verify Repayment Schedule and Due Dates
+            verifyRepaymentSchedule(loanId, //
+                    installment(0, null, "01 January 2023"), //
+                    installment(313.0, false, "31 January 2023"), //
+                    installment(313.0, false, "02 March 2023"), //
+                    installment(313.0, false, "01 April 2023"), //
+                    installment(311.0, false, "01 May 2023") //
+            );
+
+            // delete all external events
+            deleteAllExternalEvents();
+
+            // run cob
+            updateBusinessDateAndExecuteCOBJob("31 January 2023");
+
+            // verify external events
+            List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
+            Assertions.assertEquals(0, allExternalEvents.size());
+        });
+    }
+
+    @Test
+    public void testNoSnapshotEventGenerationWhenCustomSnapshotEventIsDisabled() {
+        runAt("31 January 2023", () -> {
+            // Enable Business Step
+            enableCOBBusinessStep("APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", "CHECK_LOAN_REPAYMENT_DUE",
+                    "CHECK_LOAN_REPAYMENT_OVERDUE", "UPDATE_LOAN_ARREARS_AGING", "ADD_PERIODIC_ACCRUAL_ENTRIES",
+                    "EXTERNAL_ASSET_OWNER_TRANSFER", "CHECK_DUE_INSTALLMENTS");
+
+            // Create Client
+            Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            // Create Loan Product
+            PostLoanProductsRequest loanProductsRequest = create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
+                    InterestType.FLAT, AmortizationType.EQUAL_INSTALLMENTS);
+            loanProductsRequest.setEnableInstallmentLevelDelinquency(true);
+            PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
+
+            // Apply and Approve Loan
+            Long loanId = applyAndApproveLoan(clientId, loanProductResponse.getResourceId(), "01 January 2023", 1250.0, 4);
+
+            // Disburse Loan
+            disburseLoan(loanId, BigDecimal.valueOf(1250), "01 January 2023");
+
+            // Verify Repayment Schedule and Due Dates
+            verifyRepaymentSchedule(loanId, //
+                    installment(0, null, "01 January 2023"), //
+                    installment(313.0, false, "31 January 2023"), //
+                    installment(313.0, false, "02 March 2023"), //
+                    installment(313.0, false, "01 April 2023"), //
+                    installment(311.0, false, "01 May 2023") //
+            );
+
+            // delete all external events
+            deleteAllExternalEvents();
+
+            // run cob
+            updateBusinessDateAndExecuteCOBJob("01 February 2023");
+
+            // verify external events
+            List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
+            Assertions.assertEquals(0, allExternalEvents.size());
+        });
+    }
+
+    private void deleteAllExternalEvents() {
+        ExternalEventHelper.deleteAllExternalEvents(requestSpec, createResponseSpecification(Matchers.is(204)));
+        List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
+        Assertions.assertEquals(0, allExternalEvents.size());
+    }
+
+    private void enableCOBBusinessStep(String... steps) {
+        new BusinessStepHelper().updateSteps("LOAN_CLOSE_OF_BUSINESS", steps);
+
+    }
+
+    public static String getExternalEventConfigurationsForUpdateJSON() {
+        Map<String, Map<String, Boolean>> configurationsForUpdate = new HashMap<>();
+        Map<String, Boolean> configurations = new HashMap<>();
+        configurations.put("CentersCreateBusinessEvent", true);
+        configurations.put("ClientActivateBusinessEvent", true);
+        configurationsForUpdate.put("externalEventConfigurations", configurations);
+        return new Gson().toJson(configurationsForUpdate);
+    }
+
+    private void enableLoanAccountCustomSnapshotBusinessEvent() {
+        final Map<String, Boolean> updatedConfigurations = ExternalEventConfigurationHelper.updateExternalEventConfigurations(requestSpec,
+                responseSpec, "{\"externalEventConfigurations\":{\"LoanAccountCustomSnapshotBusinessEvent\":true}}\n");
+        Assertions.assertEquals(updatedConfigurations.size(), 1);
+        Assertions.assertTrue(updatedConfigurations.containsKey("LoanAccountCustomSnapshotBusinessEvent"));
+        Assertions.assertTrue(updatedConfigurations.get("LoanAccountCustomSnapshotBusinessEvent"));
+    }
+
+    private void updateBusinessDateAndExecuteCOBJob(String date) {
+        businessDateHelper.updateBusinessDate(
+                new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));
+        schedulerJobHelper.executeAndAwaitJob("Loan COB");
+    }
+
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
index 29bdab7..42c5e1d 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
@@ -495,6 +495,11 @@
         loanAccountDelinquencyPauseChangedBusinessEvent.put("enabled", false);
         defaults.add(loanAccountDelinquencyPauseChangedBusinessEvent);
 
+        Map<String, Object> loanAccountCustomSnapshotBusinessEvent = new HashMap<>();
+        loanAccountCustomSnapshotBusinessEvent.put("type", "LoanAccountCustomSnapshotBusinessEvent");
+        loanAccountCustomSnapshotBusinessEvent.put("enabled", false);
+        defaults.add(loanAccountCustomSnapshotBusinessEvent);
+
         return defaults;
 
     }
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
index 5fc131b..261f7b9 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/Utils.java
@@ -283,6 +283,9 @@
             final String deleteURL, final String jsonAttributeToGetBack) {
         final String json = given().spec(requestSpec).expect().spec(responseSpec).log().ifError().when().delete(deleteURL).andReturn()
                 .asString();
+        if (jsonAttributeToGetBack == null) {
+            return (T) json;
+        }
         return (T) JsonPath.from(json).get(jsonAttributeToGetBack);
     }
 
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventHelper.java
new file mode 100644
index 0000000..793e144
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventHelper.java
@@ -0,0 +1,92 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests.common.externalevents;
+
+import com.google.common.reflect.TypeToken;
+import com.google.gson.Gson;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.util.List;
+import lombok.Builder;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.client.util.JSON;
+import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
+import org.apache.fineract.integrationtests.common.Utils;
+
+@Slf4j
+public final class ExternalEventHelper {
+
+    private static final Gson GSON = new JSON().getGson();
+
+    private ExternalEventHelper() {}
+
+    @Builder
+    public static class Filter {
+
+        private final String idempotencyKey;
+        private final String type;
+        private final String category;
+        private final Long aggregateRootId;
+
+        public String toQueryParams() {
+            StringBuilder stringBuilder = new StringBuilder();
+            if (idempotencyKey != null) {
+                stringBuilder.append("idempotencyKey=").append(idempotencyKey).append("&");
+            }
+
+            if (type != null) {
+                stringBuilder.append("type=").append(type).append("&");
+            }
+
+            if (category != null) {
+                stringBuilder.append("category=").append(category).append("&");
+            }
+
+            if (aggregateRootId != null) {
+                stringBuilder.append("aggregateRootId=").append(aggregateRootId).append("&");
+            }
+
+            return stringBuilder.toString();
+
+        }
+    }
+
+    public static List<ExternalEventDTO> getAllExternalEvents(final RequestSpecification requestSpec,
+            final ResponseSpecification responseSpec) {
+        final String url = "/fineract-provider/api/v1/internal/externalevents?" + Utils.TENANT_IDENTIFIER;
+        log.info("---------------------------------GETTING ALL EXTERNAL EVENTS---------------------------------------------");
+        String response = Utils.performServerGet(requestSpec, responseSpec, url);
+        return GSON.fromJson(response, new TypeToken<List<ExternalEventDTO>>() {}.getType());
+    }
+
+    public static List<ExternalEventDTO> getAllExternalEvents(final RequestSpecification requestSpec,
+            final ResponseSpecification responseSpec, Filter filter) {
+        final String url = "/fineract-provider/api/v1/internal/externalevents?" + filter.toQueryParams() + Utils.TENANT_IDENTIFIER;
+        log.info("---------------------------------GETTING ALL EXTERNAL EVENTS---------------------------------------------");
+        String response = Utils.performServerGet(requestSpec, responseSpec, url);
+        return GSON.fromJson(response, new TypeToken<List<ExternalEventDTO>>() {}.getType());
+    }
+
+    public static void deleteAllExternalEvents(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) {
+        final String url = "/fineract-provider/api/v1/internal/externalevents?" + Utils.TENANT_IDENTIFIER;
+        log.info("-----------------------------DELETE ALL EXTERNAL EVENTS PARTITIONS----------------------------------------");
+        Utils.performServerDelete(requestSpec, responseSpec, url, null);
+    }
+
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventsExtension.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventsExtension.java
new file mode 100644
index 0000000..80befb0
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/ExternalEventsExtension.java
@@ -0,0 +1,88 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests.common.externalevents;
+
+import static org.apache.fineract.integrationtests.common.Utils.initializeRESTAssured;
+
+import com.google.common.collect.MapDifference;
+import com.google.common.collect.Maps;
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.util.ArrayList;
+import java.util.Map;
+import java.util.stream.Collectors;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.integrationtests.common.ExternalEventConfigurationHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+@Slf4j
+public class ExternalEventsExtension implements AfterEachCallback, BeforeEachCallback {
+
+    private Map<String, Boolean> original;
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+
+    public ExternalEventsExtension() {
+        initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.requestSpec.header("Fineract-Platform-TenantId", "default");
+    }
+
+    @Override
+    public void afterEach(ExtensionContext context) {
+        ArrayList<Map<String, Object>> allExternalEventConfigurations = ExternalEventConfigurationHelper
+                .getAllExternalEventConfigurations(requestSpec, responseSpec);
+        Map<String, Boolean> collected = allExternalEventConfigurations.stream()
+                .map(map -> Map.entry((String) map.get("type"), (Boolean) map.get("enabled")))
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+        Map<String, MapDifference.ValueDifference<Boolean>> diff = Maps.difference(original, collected).entriesDiffering();
+        diff.keySet().forEach(key -> {
+            MapDifference.ValueDifference<Boolean> valueDifference = diff.get(key);
+            log.debug("External event {} changed from {} to {}. Restoring to its original state.", key, valueDifference.leftValue(),
+                    valueDifference.rightValue());
+            restore(key, valueDifference.leftValue());
+        });
+    }
+
+    @Override
+    public void beforeEach(ExtensionContext context) {
+        ArrayList<Map<String, Object>> allExternalEventConfigurations = ExternalEventConfigurationHelper
+                .getAllExternalEventConfigurations(requestSpec, responseSpec);
+        original = allExternalEventConfigurations.stream().map(map -> Map.entry((String) map.get("type"), (Boolean) map.get("enabled")))
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+    }
+
+    private void restore(String key, Boolean value) {
+        final Map<String, Boolean> updatedConfigurations = ExternalEventConfigurationHelper.updateExternalEventConfigurations(requestSpec,
+                responseSpec, "{\"externalEventConfigurations\":{\"" + key + "\":" + value + "}}\n");
+        Assertions.assertEquals(updatedConfigurations.size(), 1);
+        Assertions.assertTrue(updatedConfigurations.containsKey(key));
+        Assertions.assertEquals(value, updatedConfigurations.get(key));
+    }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/CobHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/CobHelper.java
index 6b4188a..9e7bb38 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/CobHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/CobHelper.java
@@ -23,16 +23,17 @@
 import java.util.List;
 import java.util.Map;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.fineract.integrationtests.client.IntegrationTest;
 import org.apache.fineract.integrationtests.common.Utils;
 
 @Slf4j
-public class CobHelper extends IntegrationTest {
+public final class CobHelper {
+
+    private CobHelper() {}
 
     public static List<Map<String, Object>> getCobPartitions(final RequestSpecification requestSpec,
             final ResponseSpecification responseSpec, int partitionSize, final String jsonReturn) {
-        final String GET_LOAN_URL = "/fineract-provider/api/v1/internal/cob/partitions/" + partitionSize + "?" + Utils.TENANT_IDENTIFIER;
+        final String url = "/fineract-provider/api/v1/internal/cob/partitions/" + partitionSize + "?" + Utils.TENANT_IDENTIFIER;
         log.info("---------------------------------GET COB PARTITIONS---------------------------------------------");
-        return Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_URL, jsonReturn);
+        return Utils.performServerGet(requestSpec, responseSpec, url, jsonReturn);
     }
 }