This is an automated email from the ASF dual-hosted git repository.

bagrijp pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 8b379695d FINERACT-1972 Custom Snapshot Event Triggered by COB
8b379695d is described below

commit 8b379695d7c60871dbfbc8ff6bc90b324100dd0f
Author: Peter Bagrij <[email protected]>
AuthorDate: Wed Jan 17 08:05:30 2024 +0100

    FINERACT-1972 Custom Snapshot Event Triggered by COB
---
 .../infrastructure/core/boot/FineractProfiles.java |   2 +
 .../api/InternalExternalEventsApiResource.java     |  52 ++--
 .../repository/ExternalEventRepository.java        |   3 +-
 .../service/InternalExternalEventService.java      | 147 ++++++++++
 .../service/validation/ExternalEventDTO.java}      |  24 +-
 .../LoanAccountCustomSnapshotBusinessEvent.java    |  18 +-
 .../tenant/module/loan/module-changelog-master.xml |   1 +
 ...1014_add_loan_account_custom_snapshot_event.xml |  33 +++
 .../fineract/cob/api/InternalCOBApiResource.java   |   3 +-
 .../api/InternalLoanAccountLockApiResource.java    |   3 +-
 .../cob/loan/CheckDueInstallmentsBusinessStep.java |  69 +++++
 .../instancemode/api/InstanceModeApiResource.java  |   3 +-
 .../s3/LocalstackS3ClientCustomizer.java           |   3 +-
 .../api/InternalClientInformationApiResource.java  |   3 +-
 .../api/InternalLoanInformationApiResource.java    |   3 +-
 .../loan/CheckDueInstallmentsBusinessStepTest.java | 161 ++++++++++
 ...nalEventConfigurationValidationServiceTest.java |   4 +-
 .../integrationtests/BaseLoanIntegrationTest.java  |   8 +-
 .../CustomSnapshotEventIntegrationTest.java        | 326 +++++++++++++++++++++
 .../common/ExternalEventConfigurationHelper.java   |   5 +
 .../fineract/integrationtests/common/Utils.java    |   3 +
 .../common/externalevents/ExternalEventHelper.java |  92 ++++++
 .../externalevents/ExternalEventsExtension.java    |  88 ++++++
 .../integrationtests/common/loans/CobHelper.java   |   9 +-
 24 files changed, 1010 insertions(+), 53 deletions(-)

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 6d0757021..97aca9634 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 final class FineractProfiles {
     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-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/api/InternalExternalEventsApiResource.java
similarity index 52%
copy from 
fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java
copy to 
fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/api/InternalExternalEventsApiResource.java
index 7402955bc..31b84f99d 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/cob/api/InternalCOBApiResource.java
+++ 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/api/InternalExternalEventsApiResource.java
@@ -16,43 +16,36 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.cob.api;
+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.PathParam;
 import jakarta.ws.rs.Produces;
-import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.QueryParam;
 import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.UriInfo;
-import java.time.LocalDate;
 import java.util.List;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.fineract.cob.data.LoanCOBPartition;
-import org.apache.fineract.cob.loan.LoanCOBConstant;
-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.serialization.ApiRequestJsonSerializationSettings;
-import 
org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
-import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+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("test")
+@Profile(FineractProfiles.TEST)
 @Component
-@Path("/v1/internal/cob")
+@Path("/v1/internal/externalevents")
 @RequiredArgsConstructor
 @Slf4j
-public class InternalCOBApiResource implements InitializingBean {
+public class InternalExternalEventsApiResource implements InitializingBean {
 
-    private final RetrieveLoanIdService retrieveLoanIdService;
-    private final ApiRequestParameterHelper apiRequestParameterHelper;
-    private final ToApiJsonSerializer<List> toApiJsonSerializerForList;
+    private final InternalExternalEventService internalExternalEventService;
+    private final DefaultToApiJsonSerializer<List<ExternalEventDTO>> 
jsonSerializer;
 
     @Override
     @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
@@ -69,14 +62,19 @@ public class InternalCOBApiResource implements 
InitializingBean {
     @GET
     @Consumes({ MediaType.APPLICATION_JSON })
     @Produces({ MediaType.APPLICATION_JSON })
-    @Path("partitions/{partitionSize}")
-    public String getCobPartitions(@Context final UriInfo uriInfo, 
@PathParam("partitionSize") int partitionSize) {
-        LocalDate businessDate = 
ThreadLocalContextUtil.getBusinessDateByType(BusinessDateType.BUSINESS_DATE);
-        log.info("RetrieveLoanCOBPartitions is called with partitionSize {} 
for {}", partitionSize, businessDate);
-        List<LoanCOBPartition> loanCOBPartitions = 
retrieveLoanIdService.retrieveLoanCOBPartitions(LoanCOBConstant.NUMBER_OF_DAYS_BEHIND,
-                businessDate, false, partitionSize);
-        final ApiRequestJsonSerializationSettings settings = 
this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
-        return toApiJsonSerializerForList.serialize(settings, 
loanCOBPartitions);
+    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 474384d2a..9f7ccbddc 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.Exter
 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 000000000..b044b3d33
--- /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/core/boot/FineractProfiles.java
 
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/validation/ExternalEventDTO.java
similarity index 56%
copy from 
fineract-core/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractProfiles.java
copy to 
fineract-core/src/main/java/org/apache/fineract/infrastructure/event/external/service/validation/ExternalEventDTO.java
index 6d0757021..dec70373e 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/event/external/service/validation/ExternalEventDTO.java
@@ -16,12 +16,26 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.infrastructure.core.boot;
+package org.apache.fineract.infrastructure.event.external.service.validation;
 
-public final class FineractProfiles {
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.util.Map;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.ToString;
 
-    public static final String LIQUIBASE_ONLY = "liquibase-only";
-    public static final String DIAGNOSTICS = "diagnostics";
+@Getter
+@AllArgsConstructor
+@ToString
+public class ExternalEventDTO {
 
-    private FineractProfiles() {}
+    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-core/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractProfiles.java
 
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanAccountCustomSnapshotBusinessEvent.java
similarity index 63%
copy from 
fineract-core/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractProfiles.java
copy to 
fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanAccountCustomSnapshotBusinessEvent.java
index 6d0757021..628429671 100644
--- 
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/boot/FineractProfiles.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/LoanAccountCustomSnapshotBusinessEvent.java
@@ -16,12 +16,20 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.fineract.infrastructure.core.boot;
+package org.apache.fineract.infrastructure.event.business.domain.loan;
 
-public final class FineractProfiles {
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
 
-    public static final String LIQUIBASE_ONLY = "liquibase-only";
-    public static final String DIAGNOSTICS = "diagnostics";
+public class LoanAccountCustomSnapshotBusinessEvent extends LoanBusinessEvent {
 
-    private FineractProfiles() {}
+    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 b08ead78b..4d5fb3f2b 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 000000000..4c1ad18b9
--- /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 7402955bc..c767a5479 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.LoanCOBConstant;
 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.beans.factory.InitializingBean;
 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 9d5d731d3..a92571978 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.LoanAccountLock;
 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 000000000..fcce7307d
--- /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 215b75463..837638f63 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.MediaType;
 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 6b764aa6a..4e7d10853 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 @@ package org.apache.fineract.infrastructure.s3;
 
 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 @@ import software.amazon.awssdk.services.s3.S3ClientBuilder;
 
 @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 6b1342b53..ae454ca08 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 java.util.Map;
 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.beans.factory.InitializingBean;
 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 7969fd98f..a15d7f4a1 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 java.util.Map;
 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.beans.factory.InitializingBean;
 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 000000000..161123c8c
--- /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 ca46595cf..69b69d9db 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 @@ public class ExternalEventConfigurationValidationServiceTest {
                 "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 @@ public class 
ExternalEventConfigurationValidationServiceTest {
                 "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 e04e414b0..dcc4c1d8a 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.integrationtests.common.loans.LoanTransactionHelper;
 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 @@ public abstract class BaseLoanIntegrationTest {
 
     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 @@ public abstract class BaseLoanIntegrationTest {
         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 000000000..d2a29aa94
--- /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 29bdab718..42c5e1dd3 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 @@ public class ExternalEventConfigurationHelper {
         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 5fc131b0e..261f7b976 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 @@ public final class Utils {
             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 000000000..793e14444
--- /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 000000000..80befb045
--- /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 6b4188a36..9e7bb38d6 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 io.restassured.specification.ResponseSpecification;
 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);
     }
 }

Reply via email to