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

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

commit 6cd634cb2fde6b73d81ef32e82d8afbec2d61b4d
Author: adam.magyari <[email protected]>
AuthorDate: Tue Jun 3 12:23:20 2025 +0200

    FINERACT-2232: Capitalized income - Business events
---
 ...lizedIncomeTransactionCreatedBusinessEvent.java |  35 +++
 .../CapitalizedIncomeWritePlatformServiceImpl.java |  12 +
 .../ProgressiveLoanAccountConfiguration.java       |   6 +-
 .../db/changelog/tenant/changelog-tenant.xml       |   1 +
 ...alizedIncomeTransactionCreatedBusinessEvent.xml |  31 +++
 ...nalEventConfigurationValidationServiceTest.java |   6 +-
 .../integrationtests/BaseLoanIntegrationTest.java  |  43 +++-
 .../CustomSnapshotEventIntegrationTest.java        |   7 -
 .../ExternalBusinessEventTest.java                 | 253 +--------------------
 .../LoanCapitalizedIncomeTest.java                 | 113 +++++++++
 .../common/ExternalEventConfigurationHelper.java   |   5 +
 .../common/externalevents/BusinessEvent.java       |  43 ++++
 .../LoanAdjustTransactionBusinessEvent.java        | 122 ++++++++++
 .../common/externalevents/LoanBusinessEvent.java   |  85 +++++++
 .../LoanTransactionBusinessEvent.java              |  63 +++++
 15 files changed, 563 insertions(+), 262 deletions(-)

diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanCapitalizedIncomeTransactionCreatedBusinessEvent.java
 
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanCapitalizedIncomeTransactionCreatedBusinessEvent.java
new file mode 100644
index 0000000000..49a7e22d13
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanCapitalizedIncomeTransactionCreatedBusinessEvent.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.transaction;
+
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+
+public class LoanCapitalizedIncomeTransactionCreatedBusinessEvent extends 
LoanTransactionBusinessEvent {
+
+    private static final String TYPE = 
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent";
+
+    public 
LoanCapitalizedIncomeTransactionCreatedBusinessEvent(LoanTransaction value) {
+        super(value);
+    }
+
+    @Override
+    public String getType() {
+        return TYPE;
+    }
+}
diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java
index 2df2b7d2b3..43894c0e3d 100644
--- 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java
@@ -34,6 +34,10 @@ import 
org.apache.fineract.infrastructure.core.domain.ExternalId;
 import org.apache.fineract.infrastructure.core.service.DateUtils;
 import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
 import org.apache.fineract.infrastructure.core.service.MathUtil;
+import 
org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
+import 
org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent;
+import 
org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeTransactionCreatedBusinessEvent;
+import 
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
 import org.apache.fineract.organisation.monetary.domain.Money;
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
@@ -61,6 +65,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl 
implements CapitalizedInc
     private final LoanCapitalizedIncomeBalanceRepository 
capitalizedIncomeBalanceRepository;
     private final ReprocessLoanTransactionsService 
reprocessLoanTransactionsService;
     private final LoanBalanceService loanBalanceService;
+    private final BusinessEventNotifierService businessEventNotifierService;
 
     @Transactional
     @Override
@@ -101,6 +106,9 @@ public class CapitalizedIncomeWritePlatformServiceImpl 
implements CapitalizedInc
         // Post journal entries
         journalEntryPoster.postJournalEntries(loan, existingTransactionIds, 
existingReversedTransactionIds);
 
+        businessEventNotifierService
+                .notifyPostBusinessEvent(new 
LoanCapitalizedIncomeTransactionCreatedBusinessEvent(capitalizedIncomeTransaction));
+        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanBalanceChangedBusinessEvent(loan));
         return new CommandProcessingResultBuilder() //
                 .withEntityId(capitalizedIncomeTransaction.getId()) //
                 
.withEntityExternalId(capitalizedIncomeTransaction.getExternalId()) //
@@ -153,6 +161,10 @@ public class CapitalizedIncomeWritePlatformServiceImpl 
implements CapitalizedInc
                 
MathUtil.negativeToZero(capitalizedIncomeBalance.getUnrecognizedAmount().subtract(transactionAmount)));
         capitalizedIncomeBalanceRepository.save(capitalizedIncomeBalance);
 
+        businessEventNotifierService.notifyPostBusinessEvent(
+                new 
LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent(savedCapitalizedIncomeAdjustment));
+        businessEventNotifierService.notifyPostBusinessEvent(new 
LoanBalanceChangedBusinessEvent(loan));
+
         return new CommandProcessingResultBuilder() //
                 .withEntityId(savedCapitalizedIncomeAdjustment.getId()) //
                 
.withEntityExternalId(savedCapitalizedIncomeAdjustment.getExternalId()) //
diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java
index 6694b9eab0..2cd92df3d2 100644
--- 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.starter;
 
 import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
 import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
+import 
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
 import 
org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
@@ -50,10 +51,11 @@ public class ProgressiveLoanAccountConfiguration {
             PaymentDetailWritePlatformService 
paymentDetailWritePlatformService, LoanJournalEntryPoster journalEntryPoster,
             NoteWritePlatformService noteWritePlatformService, 
ExternalIdFactory externalIdFactory,
             LoanCapitalizedIncomeBalanceRepository 
capitalizedIncomeBalanceRepository,
-            ReprocessLoanTransactionsService reprocessLoanTransactionsService, 
LoanBalanceService loanBalanceService) {
+            ReprocessLoanTransactionsService reprocessLoanTransactionsService, 
LoanBalanceService loanBalanceService,
+            BusinessEventNotifierService businessEventNotifierService) {
         return new 
CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, 
loanAssembler, loanTransactionRepository,
                 paymentDetailWritePlatformService, journalEntryPoster, 
noteWritePlatformService, externalIdFactory,
-                capitalizedIncomeBalanceRepository, 
reprocessLoanTransactionsService, loanBalanceService);
+                capitalizedIncomeBalanceRepository, 
reprocessLoanTransactionsService, loanBalanceService, 
businessEventNotifierService);
     }
 
     @Bean
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml 
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
index 23d39d7f9e..50b68a01f9 100644
--- 
a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml
@@ -201,4 +201,5 @@
     <include file="parts/0180_add_version_field_to_loan_transaction.xml" 
relativeToChangelogFile="true" />
     <include 
file="parts/0181_add_capitalized_income_amortization_adjustment_transaction.xml"
 relativeToChangelogFile="true" />
     <include 
file="parts/0182_transaction_summary_with_asset_owner_report_fix_charge_reason_and_add_buyback_intermediate.xml"
 relativeToChangelogFile="true" />
+    <include 
file="parts/0183_add_LoanCapitalizedIncomeTransactionCreatedBusinessEvent.xml" 
relativeToChangelogFile="true" />
 </databaseChangeLog>
diff --git 
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0183_add_LoanCapitalizedIncomeTransactionCreatedBusinessEvent.xml
 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0183_add_LoanCapitalizedIncomeTransactionCreatedBusinessEvent.xml
new file mode 100644
index 0000000000..66a28bab0c
--- /dev/null
+++ 
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0183_add_LoanCapitalizedIncomeTransactionCreatedBusinessEvent.xml
@@ -0,0 +1,31 @@
+<?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 id="1" author="fineract">
+        <insert tableName="m_external_event_configuration">
+            <column name="type" 
value="LoanCapitalizedIncomeTransactionCreatedBusinessEvent"/>
+            <column name="enabled" valueBoolean="false"/>
+        </insert>
+    </changeSet>
+</databaseChangeLog>
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 f802e918fe..5d5e47edde 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
@@ -108,7 +108,8 @@ public class 
ExternalEventConfigurationValidationServiceTest {
                 "LoanTransactionInterestRefundPreBusinessEvent", 
"LoanAccrualAdjustmentTransactionBusinessEvent",
                 
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
                 
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", 
"LoanTransactionContractTerminationPostBusinessEvent",
-                
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent");
+                
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
+                "LoanCapitalizedIncomeTransactionCreatedBusinessEvent");
 
         List<FineractPlatformTenant> tenants = Arrays
                 .asList(new FineractPlatformTenant(1L, "default", "Default 
Tenant", "Europe/Budapest", null));
@@ -197,7 +198,8 @@ public class 
ExternalEventConfigurationValidationServiceTest {
                 "LoanTransactionInterestRefundPreBusinessEvent", 
"LoanAccrualAdjustmentTransactionBusinessEvent",
                 
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
                 
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent", 
"LoanTransactionContractTerminationPostBusinessEvent",
-                
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent");
+                
"LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent",
+                "LoanCapitalizedIncomeTransactionCreatedBusinessEvent");
 
         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 8659659b80..201e41caf7 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
@@ -87,6 +87,7 @@ import 
org.apache.fineract.client.models.RetrieveLoansPointInTimeRequest;
 import org.apache.fineract.client.util.CallFailedRuntimeException;
 import org.apache.fineract.client.util.Calls;
 import 
org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
+import 
org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
 import org.apache.fineract.integrationtests.client.IntegrationTest;
 import org.apache.fineract.integrationtests.common.BatchHelper;
 import org.apache.fineract.integrationtests.common.BusinessDateHelper;
@@ -99,6 +100,9 @@ import 
org.apache.fineract.integrationtests.common.accounting.AccountHelper;
 import 
org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper;
 import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
 import org.apache.fineract.integrationtests.common.error.ErrorResponse;
+import 
org.apache.fineract.integrationtests.common.externalevents.BusinessEvent;
+import 
org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper;
+import 
org.apache.fineract.integrationtests.common.externalevents.ExternalEventsExtension;
 import org.apache.fineract.integrationtests.common.loans.LoanAccountLockHelper;
 import org.apache.fineract.integrationtests.common.loans.LoanProductHelper;
 import 
org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
@@ -119,7 +123,7 @@ import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.extension.ExtendWith;
 
 @Slf4j
-@ExtendWith(LoanTestLifecycleExtension.class)
+@ExtendWith({ LoanTestLifecycleExtension.class, ExternalEventsExtension.class 
})
 public abstract class BaseLoanIntegrationTest extends IntegrationTest {
 
     protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
@@ -171,6 +175,7 @@ public abstract class BaseLoanIntegrationTest extends 
IntegrationTest {
     protected GlobalConfigurationHelper globalConfigurationHelper = new 
GlobalConfigurationHelper();
     protected final CodeHelper codeHelper = new CodeHelper();
     protected final ChargesHelper chargesHelper = new ChargesHelper();
+    protected final ExternalEventHelper externalEventHelper = new 
ExternalEventHelper();
 
     protected static void validateRepaymentPeriod(GetLoansLoanIdResponse 
loanDetails, Integer index, LocalDate dueDate, double principalDue,
             double principalPaid, double principalOutstanding, double 
paidInAdvance, double paidLate) {
@@ -1269,6 +1274,42 @@ public abstract class BaseLoanIntegrationTest extends 
IntegrationTest {
                 new 
PostLoansLoanIdRequest().rejectedOnDate(rejectedOnDate).locale("en").dateFormat(DATETIME_PATTERN));
     }
 
+    protected void verifyBusinessEvents(BusinessEvent... businessEvents) {
+        List<ExternalEventDTO> allExternalEvents = 
ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
+        logBusinessEvents(allExternalEvents);
+        Assertions.assertNotNull(businessEvents);
+        Assertions.assertNotNull(allExternalEvents);
+        Assertions.assertTrue(businessEvents.length <= 
allExternalEvents.size(),
+                "Expected business event count is less than actual. Expected: 
" + businessEvents.length + " Actual: "
+                        + allExternalEvents.size());
+        final DateTimeFormatter formatter = 
DateTimeFormatter.ofPattern(DATETIME_PATTERN, Locale.ENGLISH);
+        for (BusinessEvent businessEvent : businessEvents) {
+            long count = allExternalEvents.stream().filter(externalEvent -> 
businessEvent.verify(externalEvent, formatter)).count();
+            Assertions.assertEquals(1, count, "Expected business event not 
found " + businessEvent);
+        }
+    }
+
+    protected void logBusinessEvents(List<ExternalEventDTO> allExternalEvents) 
{
+        allExternalEvents.forEach(externalEventDTO -> {
+            Object amount = externalEventDTO.getPayLoad().get("amount");
+            Object outstandingLoanBalance = 
externalEventDTO.getPayLoad().get("outstandingLoanBalance");
+            Object principalPortion = 
externalEventDTO.getPayLoad().get("principalPortion");
+            Object interestPortion = 
externalEventDTO.getPayLoad().get("interestPortion");
+            Object feePortion = 
externalEventDTO.getPayLoad().get("feeChargesPortion");
+            Object penaltyPortion = 
externalEventDTO.getPayLoad().get("penaltyChargesPortion");
+            log.info("Event Received\n type:'{}'\n businessDate:'{}'", 
externalEventDTO.getType(), externalEventDTO.getBusinessDate());
+            log.info(
+                    "Values\n amount: {}\n outstandingLoanBalance: {}\n 
principalPortion: {}\n interestPortion: {}\n feePortion: {}\n penaltyPortion: 
{}",
+                    amount, outstandingLoanBalance, principalPortion, 
interestPortion, feePortion, penaltyPortion);
+        });
+    }
+
+    protected void deleteAllExternalEvents() {
+        ExternalEventHelper.deleteAllExternalEvents(requestSpec, 
createResponseSpecification(Matchers.is(204)));
+        List<ExternalEventDTO> allExternalEvents = 
ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
+        Assertions.assertEquals(0, allExternalEvents.size());
+    }
+
     @RequiredArgsConstructor
     public static class BatchRequestBuilder {
 
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
index abb535e37e..59786806af 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/CustomSnapshotEventIntegrationTest.java
@@ -37,7 +37,6 @@ 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;
@@ -305,12 +304,6 @@ public class CustomSnapshotEventIntegrationTest extends 
BaseLoanIntegrationTest
         });
     }
 
-    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);
 
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
index 715ca3e936..10988e91c7 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java
@@ -25,24 +25,12 @@ import io.restassured.builder.ResponseSpecBuilder;
 import io.restassured.http.ContentType;
 import io.restassured.specification.RequestSpecification;
 import io.restassured.specification.ResponseSpecification;
-import jakarta.validation.constraints.NotNull;
 import java.math.BigDecimal;
-import java.time.LocalDate;
-import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
-import java.util.Objects;
 import java.util.concurrent.atomic.AtomicReference;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.EqualsAndHashCode;
-import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang3.StringUtils;
 import org.apache.fineract.client.models.GetLoansLoanIdResponse;
 import org.apache.fineract.client.models.GetLoansLoanIdStatus;
 import org.apache.fineract.client.models.GlobalConfigurationPropertyData;
@@ -66,8 +54,10 @@ import 
org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
 import org.apache.fineract.integrationtests.common.Utils;
 import 
org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper;
 import 
org.apache.fineract.integrationtests.common.externalevents.ExternalEventsExtension;
+import 
org.apache.fineract.integrationtests.common.externalevents.LoanAdjustTransactionBusinessEvent;
+import 
org.apache.fineract.integrationtests.common.externalevents.LoanBusinessEvent;
+import 
org.apache.fineract.integrationtests.common.externalevents.LoanTransactionBusinessEvent;
 import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
-import org.hamcrest.Matchers;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
@@ -1012,12 +1002,6 @@ public class ExternalBusinessEventTest extends 
BaseLoanIntegrationTest {
         
externalEventHelper.disableBusinessEvent("LoanBalanceChangedBusinessEvent");
     }
 
-    private void deleteAllExternalEvents() {
-        ExternalEventHelper.deleteAllExternalEvents(requestSpec, 
createResponseSpecification(Matchers.is(204)));
-        List<ExternalEventDTO> allExternalEvents = 
ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
-        Assertions.assertEquals(0, allExternalEvents.size());
-    }
-
     private static Long createLoanProductPeriodicWithInterest() {
         String name = Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6);
         String shortName = Utils.uniqueRandomStringGenerator("", 4);
@@ -1070,21 +1054,6 @@ public class ExternalBusinessEventTest extends 
BaseLoanIntegrationTest {
         return loanId;
     }
 
-    private void logBusinessEvents(List<ExternalEventDTO> allExternalEvents) {
-        allExternalEvents.forEach(externalEventDTO -> {
-            Object amount = externalEventDTO.getPayLoad().get("amount");
-            Object outstandingLoanBalance = 
externalEventDTO.getPayLoad().get("outstandingLoanBalance");
-            Object principalPortion = 
externalEventDTO.getPayLoad().get("principalPortion");
-            Object interestPortion = 
externalEventDTO.getPayLoad().get("interestPortion");
-            Object feePortion = 
externalEventDTO.getPayLoad().get("feeChargesPortion");
-            Object penaltyPortion = 
externalEventDTO.getPayLoad().get("penaltyChargesPortion");
-            log.info("Event Received\n type:'{}'\n businessDate:'{}'", 
externalEventDTO.getType(), externalEventDTO.getBusinessDate());
-            log.info(
-                    "Values\n amount: {}\n outstandingLoanBalance: {}\n 
principalPortion: {}\n interestPortion: {}\n feePortion: {}\n penaltyPortion: 
{}",
-                    amount, outstandingLoanBalance, principalPortion, 
interestPortion, feePortion, penaltyPortion);
-        });
-    }
-
     private void enableLoanInterestRefundPstBusinessEvent(boolean enabled) {
         
externalEventHelper.configureBusinessEvent("LoanTransactionInterestRefundPostBusinessEvent",
 enabled);
     }
@@ -1096,220 +1065,4 @@ public class ExternalBusinessEventTest extends 
BaseLoanIntegrationTest {
     private void configureLoanAccrualTransactionCreatedBusinessEvent(boolean 
enabled) {
         
externalEventHelper.configureBusinessEvent("LoanAccrualTransactionCreatedBusinessEvent",
 enabled);
     }
-
-    public void verifyBusinessEvents(BusinessEvent... businessEvents) {
-        List<ExternalEventDTO> allExternalEvents = 
ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
-        logBusinessEvents(allExternalEvents);
-        Assertions.assertNotNull(businessEvents);
-        Assertions.assertNotNull(allExternalEvents);
-        Assertions.assertTrue(businessEvents.length <= 
allExternalEvents.size(),
-                "Expected business event count is less than actual. Expected: 
" + businessEvents.length + " Actual: "
-                        + allExternalEvents.size());
-        final DateTimeFormatter formatter = 
DateTimeFormatter.ofPattern(DATETIME_PATTERN, Locale.ENGLISH);
-        for (BusinessEvent businessEvent : businessEvents) {
-            long count = allExternalEvents.stream().filter(externalEvent -> 
businessEvent.verify(externalEvent, formatter)).count();
-            Assertions.assertEquals(1, count, "Expected business event not 
found " + businessEvent);
-        }
-    }
-
-    @Data
-    @AllArgsConstructor
-    @NoArgsConstructor
-    public static class BusinessEvent {
-
-        String type;
-        String businessDate;
-
-        boolean verify(@NotNull ExternalEventDTO externalEvent, 
DateTimeFormatter formatter) {
-            var businessDate = LocalDate.parse(getBusinessDate(), formatter);
-
-            return Objects.equals(externalEvent.getType(), getType()) && 
Objects.equals(externalEvent.getBusinessDate(), businessDate);
-        }
-    }
-
-    @EqualsAndHashCode(callSuper = true)
-    @Data
-    @AllArgsConstructor
-    public static class LoanTransactionBusinessEvent extends BusinessEvent {
-
-        private Double amount;
-        private Double outstandingLoanBalance;
-        private Double principalPortion;
-        private Double interestPortion;
-        private Double feeChargesPortion;
-        private Double penaltyChargesPortion;
-
-        public LoanTransactionBusinessEvent(String type, String businessDate, 
Double amount, Double outstandingLoanBalance,
-                Double principalPortion, Double interestPortion, Double 
feeChargesPortion, Double penaltyChargesPortion) {
-            super(type, businessDate);
-            this.amount = amount;
-            this.outstandingLoanBalance = outstandingLoanBalance;
-            this.principalPortion = principalPortion;
-            this.interestPortion = interestPortion;
-            this.feeChargesPortion = feeChargesPortion;
-            this.penaltyChargesPortion = penaltyChargesPortion;
-        }
-
-        @Override
-        boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter 
formatter) {
-            Object amount = externalEvent.getPayLoad().get("amount");
-            Object outstandingLoanBalance = 
externalEvent.getPayLoad().get("outstandingLoanBalance");
-            Object principalPortion = 
externalEvent.getPayLoad().get("principalPortion");
-            Object interestPortion = 
externalEvent.getPayLoad().get("interestPortion");
-            Object feePortion = 
externalEvent.getPayLoad().get("feeChargesPortion");
-            Object penaltyPortion = 
externalEvent.getPayLoad().get("penaltyChargesPortion");
-
-            return super.verify(externalEvent, formatter) && 
Objects.equals(amount, getAmount())
-                    && Objects.equals(outstandingLoanBalance, 
getOutstandingLoanBalance())
-                    && Objects.equals(principalPortion, getPrincipalPortion()) 
&& Objects.equals(interestPortion, getInterestPortion())
-                    && Objects.equals(feePortion, getFeeChargesPortion()) && 
Objects.equals(penaltyPortion, getPenaltyChargesPortion());
-        }
-    }
-
-    @EqualsAndHashCode(callSuper = true)
-    @Data
-    @AllArgsConstructor
-    public static class LoanBusinessEvent extends BusinessEvent {
-
-        private Integer statusId;
-        private Double principalDisbursed;
-        private Double principalOutstanding;
-        private List<String> loanTermVariationType;
-
-        public LoanBusinessEvent(String type, String businessDate, Integer 
statusId, Double principalDisbursed,
-                Double principalOutstanding) {
-            super(type, businessDate);
-            this.statusId = statusId;
-            this.principalDisbursed = principalDisbursed;
-            this.principalOutstanding = principalOutstanding;
-        }
-
-        public LoanBusinessEvent(String type, String businessDate, Integer 
statusId, Double principalDisbursed, Double principalOutstanding,
-                List<String> loanTermVariationType) {
-            super(type, businessDate);
-            this.statusId = statusId;
-            this.principalDisbursed = principalDisbursed;
-            this.principalOutstanding = principalOutstanding;
-            this.loanTermVariationType = loanTermVariationType;
-        }
-
-        @Override
-        public boolean verify(ExternalEventDTO externalEvent, 
DateTimeFormatter formatter) {
-            Object summaryRes = externalEvent.getPayLoad().get("summary");
-            Object statusRes = externalEvent.getPayLoad().get("status");
-            Map<String, Object> summary = summaryRes instanceof Map ? 
(Map<String, Object>) summaryRes : Map.of();
-            Map<String, Object> status = statusRes instanceof Map ? 
(Map<String, Object>) statusRes : Map.of();
-            var principalDisbursed = summary.get("principalDisbursed");
-
-            var principalOutstanding = summary.get("principalOutstanding");
-            Double statusId = (Double) status.get("id");
-            return super.verify(externalEvent, formatter) && 
Objects.equals(statusId, getStatusId().doubleValue())
-                    && Objects.equals(principalDisbursed, 
getPrincipalDisbursed())
-                    && Objects.equals(principalOutstanding, 
getPrincipalOutstanding()) && loanTermVariationsMatch(
-                            (List<Map<String, Object>>) 
externalEvent.getPayLoad().get("loanTermVariations"), loanTermVariationType);
-        }
-
-        private boolean loanTermVariationsMatch(final List<Map<String, 
Object>> loanTermVariations, final List<String> expectedTypes) {
-            if (CollectionUtils.isEmpty(expectedTypes)) {
-                return true;
-            }
-            final long numberOfMatches = 
expectedTypes.stream().filter(expectedType -> 
loanTermVariations.stream().anyMatch(
-                    variation -> StringUtils.equals((String) ((Map<String, 
Object>) variation.get("termType")).get("value"), expectedType)))
-                    .count();
-
-            return numberOfMatches == expectedTypes.size();
-        }
-    }
-
-    public static class LoanAdjustTransactionBusinessEvent extends 
BusinessEvent {
-
-        private String transactionTypeCode;
-        private String transactionDate;
-        private Double oldAmount;
-        private Double newAmount;
-        private Double oldPrincipalPortion;
-        private Double newPrincipalPortion;
-        private Double oldInterestPortion;
-        private Double newInterestPortion;
-        private Double oldFeePortion;
-        private Double newFeePortion;
-        private Double oldPenaltyPortion;
-        private Double newPenaltyPortion;
-
-        public LoanAdjustTransactionBusinessEvent(String type, String 
businessDate, String transactionTypeCode, String transactionDate) {
-            super(type, businessDate);
-            this.transactionTypeCode = transactionTypeCode;
-            this.transactionDate = transactionDate;
-        }
-
-        public LoanAdjustTransactionBusinessEvent(String type, String 
businessDate, String transactionTypeCode, String transactionDate,
-                Double oldAmount, Double newAmount) {
-            super(type, businessDate);
-            this.transactionTypeCode = transactionTypeCode;
-            this.transactionDate = transactionDate;
-            this.oldAmount = oldAmount;
-            this.newAmount = newAmount;
-        }
-
-        public LoanAdjustTransactionBusinessEvent(String type, String 
businessDate, String transactionTypeCode, String transactionDate,
-                Double oldAmount, Double newAmount, Double 
oldPrincipalPortion, Double newPrincipalPortion, Double oldInterestPortion,
-                Double newInterestPortion, Double oldFeePortion, Double 
newFeePortion, Double oldPenaltyPortion, Double newPenaltyPortion) {
-            super(type, businessDate);
-            this.transactionTypeCode = transactionTypeCode;
-            this.transactionDate = transactionDate;
-            this.oldAmount = oldAmount;
-            this.newAmount = newAmount;
-            this.oldPrincipalPortion = oldPrincipalPortion;
-            this.newPrincipalPortion = newPrincipalPortion;
-            this.oldInterestPortion = oldInterestPortion;
-            this.newInterestPortion = newInterestPortion;
-            this.oldFeePortion = oldFeePortion;
-            this.newFeePortion = newFeePortion;
-            this.oldPenaltyPortion = oldPenaltyPortion;
-            this.newPenaltyPortion = newPenaltyPortion;
-        }
-
-        @Override
-        boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter 
formatter) {
-            final Object transactionToAdjust = 
externalEvent.getPayLoad().get("transactionToAdjust");
-            final Map<?, Object> transActionToAdjustMap = transactionToAdjust 
instanceof Map ? (Map<String, Object>) transactionToAdjust
-                    : Collections.emptyMap();
-
-            Object actualOldAmount = transActionToAdjustMap.get("amount");
-            Object actualOldPrincipalPortion = 
transActionToAdjustMap.get("principalPortion");
-            Object actualOldInterestPortion = 
transActionToAdjustMap.get("interestPortion");
-            Object actualOldFeePortion = 
transActionToAdjustMap.get("feeChargesPortion");
-            Object actualOldPenaltyPortion = 
transActionToAdjustMap.get("penaltyChargesPortion");
-
-            final Object newTransactionDetail = 
externalEvent.getPayLoad().get("newTransactionDetail");
-            final Map<?, Object> newTransactionDetailMap = 
newTransactionDetail instanceof Map ? (Map<String, Object>) newTransactionDetail
-                    : Collections.emptyMap();
-
-            Object actualNewAmount = newTransactionDetailMap.get("amount");
-            Object actualNewPrincipalPortion = 
newTransactionDetailMap.get("principalPortion");
-            Object actualNewInterestPortion = 
newTransactionDetailMap.get("interestPortion");
-            Object actualNewFeePortion = 
newTransactionDetailMap.get("feeChargesPortion");
-            Object actualNewPenaltyPortion = 
newTransactionDetailMap.get("penaltyChargesPortion");
-
-            final Object actualTransactionDate = 
transActionToAdjustMap.get("date");
-            final Object transactionType = transActionToAdjustMap.get("type");
-            final Map<?, Object> transactionTypeMap = transactionType 
instanceof Map ? (Map<String, Object>) transactionType
-                    : Collections.emptyMap();
-            final Object actualTransactionTypeCode = 
transactionTypeMap.get("code");
-
-            return super.verify(externalEvent, formatter)//
-                    && Objects.equals(actualTransactionTypeCode, 
transactionTypeCode)
-                    && Objects.equals(actualTransactionDate, transactionDate)//
-                    && (oldAmount == null || Objects.equals(actualOldAmount, 
oldAmount))//
-                    && (newAmount == null || Objects.equals(actualNewAmount, 
newAmount))//
-                    && (oldPrincipalPortion == null || 
Objects.equals(actualOldPrincipalPortion, oldPrincipalPortion))//
-                    && (newPrincipalPortion == null || 
Objects.equals(actualNewPrincipalPortion, newPrincipalPortion))//
-                    && (oldInterestPortion == null || 
Objects.equals(actualOldInterestPortion, oldInterestPortion))//
-                    && (newInterestPortion == null || 
Objects.equals(actualNewInterestPortion, newInterestPortion))//
-                    && (oldFeePortion == null || 
Objects.equals(actualOldFeePortion, oldFeePortion))//
-                    && (newFeePortion == null || 
Objects.equals(actualNewFeePortion, newFeePortion))//
-                    && (oldPenaltyPortion == null || 
Objects.equals(actualOldPenaltyPortion, oldPenaltyPortion))//
-                    && (newPenaltyPortion == null || 
Objects.equals(actualNewPenaltyPortion, newPenaltyPortion));
-        }
-    }
 }
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java
index 28b18e112d..b2d1ae2c4a 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java
@@ -36,6 +36,9 @@ import org.apache.fineract.client.models.PostLoansResponse;
 import org.apache.fineract.client.util.CallFailedRuntimeException;
 import org.apache.fineract.integrationtests.common.BusinessStepHelper;
 import org.apache.fineract.integrationtests.common.ClientHelper;
+import 
org.apache.fineract.integrationtests.common.externalevents.LoanAdjustTransactionBusinessEvent;
+import 
org.apache.fineract.integrationtests.common.externalevents.LoanBusinessEvent;
+import 
org.apache.fineract.integrationtests.common.externalevents.LoanTransactionBusinessEvent;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
@@ -780,4 +783,114 @@ public class LoanCapitalizedIncomeTest extends 
BaseLoanIntegrationTest {
         Assertions.assertEquals(thousandFiveHundred, 
loanDetails.getSummary().getTotalPrincipal().setScale(1, RoundingMode.HALF_UP));
         Assertions.assertEquals(zero, 
loanDetails.getSummary().getPrincipalOutstanding().setScale(0, 
RoundingMode.HALF_UP));
     }
+
+    @Test
+    public void testCapitalizedIncomeEvents() {
+        
externalEventHelper.enableBusinessEvent("LoanCapitalizedIncomeTransactionCreatedBusinessEvent");
+        
externalEventHelper.enableBusinessEvent("LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent");
+        
externalEventHelper.enableBusinessEvent("LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent");
+        
externalEventHelper.enableBusinessEvent("LoanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent");
+        
externalEventHelper.enableBusinessEvent("LoanAdjustTransactionBusinessEvent");
+        
externalEventHelper.enableBusinessEvent("LoanBalanceChangedBusinessEvent");
+
+        final AtomicReference<Long> loanIdRef = new AtomicReference<>();
+        final AtomicReference<Long> capitalizedIncomeTransactionIdRef = new 
AtomicReference<>();
+
+        final PostClientsResponse client = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+
+        final PostLoanProductsResponse loanProductsResponse = loanProductHelper
+                
.createLoanProduct(create4IProgressive().enableIncomeCapitalization(true)
+                        
.capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)
+                        
.capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)
+                        
.deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue())
+                        
.incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue())
+                        
.capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE));
+
+        runAt("1 January 2024", () -> {
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductsResponse.getResourceId(), "1 January 2024",
+                    500.0, 7.0, 3, null);
+            loanIdRef.set(loanId);
+
+            disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
+
+            deleteAllExternalEvents();
+
+            Long capitalizedIncomeTransactionId = 
loanTransactionHelper.addCapitalizedIncome(loanId, "1 January 2024", 100.0)
+                    .getResourceId();
+            
capitalizedIncomeTransactionIdRef.set(capitalizedIncomeTransactionId);
+
+            verifyBusinessEvents(
+                    new 
LoanTransactionBusinessEvent("LoanCapitalizedIncomeTransactionCreatedBusinessEvent",
 "01 January 2024", 100.0,
+                            200.0, 100.0, 0.0, 0.0, 0.0),
+                    new LoanBusinessEvent("LoanBalanceChangedBusinessEvent", 
"01 January 2024", 300, 100.0, 200.0));
+        });
+        runAt("2 January 2024", () -> {
+            Long loanId = loanIdRef.get();
+
+            deleteAllExternalEvents();
+
+            executeInlineCOB(loanId);
+
+            verifyTransactions(loanId, //
+                    transaction(100.0, "Disbursement", "01 January 2024"), //
+                    transaction(100.0, "Capitalized Income", "01 January 
2024"), //
+                    transaction(1.10, "Capitalized Income Amortization", "01 
January 2024") //
+            );
+            verifyBusinessEvents(new 
LoanTransactionBusinessEvent("LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
+                    "01 January 2024", 1.10, 0.0, 0.0, 0.0, 1.10, 0.0));
+        });
+        runAt("3 January 2024", () -> {
+            Long loanId = loanIdRef.get();
+            executeInlineCOB(loanId);
+
+            deleteAllExternalEvents();
+
+            Long capitalizedIncomeAdjustmentTransactionId = 
loanTransactionHelper
+                    .capitalizedIncomeAdjustment(loanId, 
capitalizedIncomeTransactionIdRef.get(), "3 January 2024", 
50.0).getResourceId();
+
+            verifyTransactions(loanId, //
+                    transaction(100.0, "Disbursement", "01 January 2024"), //
+                    transaction(100.0, "Capitalized Income", "01 January 
2024"), //
+                    transaction(1.10, "Capitalized Income Amortization", "01 
January 2024"), //
+                    transaction(0.04, "Accrual", "02 January 2024"), //
+                    transaction(1.10, "Capitalized Income Amortization", "02 
January 2024"), //
+                    transaction(50.0, "Capitalized Income Adjustment", "03 
January 2024") //
+            );
+
+            verifyBusinessEvents(
+                    new 
LoanTransactionBusinessEvent("LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent",
 "03 January 2024",
+                            50.0, 150.0, 50.0, 0.0, 0.0, 0.0),
+                    new LoanBusinessEvent("LoanBalanceChangedBusinessEvent", 
"03 January 2024", 300, 100.0, 150.0));
+
+            deleteAllExternalEvents();
+
+            loanTransactionHelper.reverseLoanTransaction(loanId, 
capitalizedIncomeAdjustmentTransactionId, "3 January 2024");
+
+            verifyBusinessEvents(new 
LoanAdjustTransactionBusinessEvent("LoanAdjustTransactionBusinessEvent", "03 
January 2024",
+                    "loanTransactionType.capitalizedIncomeAdjustment", 
"2024-01-03"));
+        });
+        runAt("4 January 2024", () -> {
+            Long loanId = loanIdRef.get();
+            executeInlineCOB(loanId);
+
+            deleteAllExternalEvents();
+
+            loanTransactionHelper.reverseLoanTransaction(loanId, 
capitalizedIncomeTransactionIdRef.get(), "3 January 2024");
+
+            verifyTransactions(loanId, //
+                    transaction(100.0, "Disbursement", "01 January 2024"), //
+                    transaction(100.0, "Capitalized Income", "01 January 
2024"), //
+                    transaction(1.10, "Capitalized Income Amortization", "01 
January 2024"), //
+                    transaction(0.04, "Accrual", "02 January 2024"), //
+                    transaction(1.10, "Capitalized Income Amortization", "02 
January 2024"), //
+                    transaction(0.04, "Accrual", "03 January 2024"), //
+                    transaction(1.10, "Capitalized Income Amortization", "02 
January 2024"), //
+                    transaction(50.0, "Capitalized Income Adjustment", "03 
January 2024") //
+            );
+
+            verifyBusinessEvents(new 
LoanAdjustTransactionBusinessEvent("LoanAdjustTransactionBusinessEvent", "04 
January 2024",
+                    "loanTransactionType.capitalizedIncome", "2024-01-01") //
+            );
+        });
+    }
 }
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 1e2461b9b0..acbadd414d 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
@@ -606,6 +606,11 @@ public class ExternalEventConfigurationHelper {
         
loanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent.put("enabled",
 false);
         
defaults.add(loanCapitalizedIncomeAmortizationAdjustmentTransactionCreatedBusinessEvent);
 
+        Map<String, Object> 
loanCapitalizedIncomeTransactionCreatedBusinessEvent = new HashMap<>();
+        loanCapitalizedIncomeTransactionCreatedBusinessEvent.put("type", 
"LoanCapitalizedIncomeTransactionCreatedBusinessEvent");
+        loanCapitalizedIncomeTransactionCreatedBusinessEvent.put("enabled", 
false);
+        defaults.add(loanCapitalizedIncomeTransactionCreatedBusinessEvent);
+
         return defaults;
     }
 
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/BusinessEvent.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/BusinessEvent.java
new file mode 100644
index 0000000000..340648b13b
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/BusinessEvent.java
@@ -0,0 +1,43 @@
+/**
+ * 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 jakarta.validation.constraints.NotNull;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Objects;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import 
org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class BusinessEvent {
+
+    protected String type;
+    protected String businessDate;
+
+    public boolean verify(@NotNull ExternalEventDTO externalEvent, 
DateTimeFormatter formatter) {
+        var businessDate = LocalDate.parse(getBusinessDate(), formatter);
+
+        return Objects.equals(externalEvent.getType(), getType()) && 
Objects.equals(externalEvent.getBusinessDate(), businessDate);
+    }
+}
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/LoanAdjustTransactionBusinessEvent.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/LoanAdjustTransactionBusinessEvent.java
new file mode 100644
index 0000000000..2d725b1a87
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/LoanAdjustTransactionBusinessEvent.java
@@ -0,0 +1,122 @@
+/**
+ * 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 java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Objects;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import 
org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class LoanAdjustTransactionBusinessEvent extends BusinessEvent {
+
+    private String transactionTypeCode;
+    private String transactionDate;
+    private Double oldAmount;
+    private Double newAmount;
+    private Double oldPrincipalPortion;
+    private Double newPrincipalPortion;
+    private Double oldInterestPortion;
+    private Double newInterestPortion;
+    private Double oldFeePortion;
+    private Double newFeePortion;
+    private Double oldPenaltyPortion;
+    private Double newPenaltyPortion;
+
+    // minimum data for checking if transaction was reversed
+    public LoanAdjustTransactionBusinessEvent(String type, String 
businessDate, String transactionTypeCode, String transactionDate) {
+        super(type, businessDate);
+        this.transactionTypeCode = transactionTypeCode;
+        this.transactionDate = transactionDate;
+    }
+
+    // minimum data for checking if transaction was adjusted
+    public LoanAdjustTransactionBusinessEvent(String type, String 
businessDate, String transactionTypeCode, String transactionDate,
+            Double oldAmount, Double newAmount) {
+        super(type, businessDate);
+        this.transactionTypeCode = transactionTypeCode;
+        this.transactionDate = transactionDate;
+        this.oldAmount = oldAmount;
+        this.newAmount = newAmount;
+    }
+
+    public LoanAdjustTransactionBusinessEvent(String type, String 
businessDate, String transactionTypeCode, String transactionDate,
+            Double oldAmount, Double newAmount, Double oldPrincipalPortion, 
Double newPrincipalPortion, Double oldInterestPortion,
+            Double newInterestPortion, Double oldFeePortion, Double 
newFeePortion, Double oldPenaltyPortion, Double newPenaltyPortion) {
+        super(type, businessDate);
+        this.transactionTypeCode = transactionTypeCode;
+        this.transactionDate = transactionDate;
+        this.oldAmount = oldAmount;
+        this.newAmount = newAmount;
+        this.oldPrincipalPortion = oldPrincipalPortion;
+        this.newPrincipalPortion = newPrincipalPortion;
+        this.oldInterestPortion = oldInterestPortion;
+        this.newInterestPortion = newInterestPortion;
+        this.oldFeePortion = oldFeePortion;
+        this.newFeePortion = newFeePortion;
+        this.oldPenaltyPortion = oldPenaltyPortion;
+        this.newPenaltyPortion = newPenaltyPortion;
+    }
+
+    @Override
+    public boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter 
formatter) {
+        final Object transactionToAdjust = 
externalEvent.getPayLoad().get("transactionToAdjust");
+        final Map<?, Object> transActionToAdjustMap = transactionToAdjust 
instanceof Map ? (Map<String, Object>) transactionToAdjust
+                : Collections.emptyMap();
+
+        Object actualOldAmount = transActionToAdjustMap.get("amount");
+        Object actualOldPrincipalPortion = 
transActionToAdjustMap.get("principalPortion");
+        Object actualOldInterestPortion = 
transActionToAdjustMap.get("interestPortion");
+        Object actualOldFeePortion = 
transActionToAdjustMap.get("feeChargesPortion");
+        Object actualOldPenaltyPortion = 
transActionToAdjustMap.get("penaltyChargesPortion");
+
+        final Object newTransactionDetail = 
externalEvent.getPayLoad().get("newTransactionDetail");
+        final Map<?, Object> newTransactionDetailMap = newTransactionDetail 
instanceof Map ? (Map<String, Object>) newTransactionDetail
+                : Collections.emptyMap();
+
+        Object actualNewAmount = newTransactionDetailMap.get("amount");
+        Object actualNewPrincipalPortion = 
newTransactionDetailMap.get("principalPortion");
+        Object actualNewInterestPortion = 
newTransactionDetailMap.get("interestPortion");
+        Object actualNewFeePortion = 
newTransactionDetailMap.get("feeChargesPortion");
+        Object actualNewPenaltyPortion = 
newTransactionDetailMap.get("penaltyChargesPortion");
+
+        final Object actualTransactionDate = 
transActionToAdjustMap.get("date");
+        final Object transactionType = transActionToAdjustMap.get("type");
+        final Map<?, Object> transactionTypeMap = transactionType instanceof 
Map ? (Map<String, Object>) transactionType
+                : Collections.emptyMap();
+        final Object actualTransactionTypeCode = 
transactionTypeMap.get("code");
+
+        return super.verify(externalEvent, formatter)//
+                && Objects.equals(actualTransactionTypeCode, 
transactionTypeCode) && Objects.equals(actualTransactionDate, transactionDate)//
+                && (oldAmount == null || Objects.equals(actualOldAmount, 
oldAmount))//
+                && (newAmount == null || Objects.equals(actualNewAmount, 
newAmount))//
+                && (oldPrincipalPortion == null || 
Objects.equals(actualOldPrincipalPortion, oldPrincipalPortion))//
+                && (newPrincipalPortion == null || 
Objects.equals(actualNewPrincipalPortion, newPrincipalPortion))//
+                && (oldInterestPortion == null || 
Objects.equals(actualOldInterestPortion, oldInterestPortion))//
+                && (newInterestPortion == null || 
Objects.equals(actualNewInterestPortion, newInterestPortion))//
+                && (oldFeePortion == null || 
Objects.equals(actualOldFeePortion, oldFeePortion))//
+                && (newFeePortion == null || 
Objects.equals(actualNewFeePortion, newFeePortion))//
+                && (oldPenaltyPortion == null || 
Objects.equals(actualOldPenaltyPortion, oldPenaltyPortion))//
+                && (newPenaltyPortion == null || 
Objects.equals(actualNewPenaltyPortion, newPenaltyPortion));
+    }
+}
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/LoanBusinessEvent.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/LoanBusinessEvent.java
new file mode 100644
index 0000000000..da683f2c1b
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/LoanBusinessEvent.java
@@ -0,0 +1,85 @@
+/**
+ * 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 java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import 
org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class LoanBusinessEvent extends BusinessEvent {
+
+    private Integer statusId;
+    private Double principalDisbursed;
+    private Double principalOutstanding;
+    private List<String> loanTermVariationType;
+
+    public LoanBusinessEvent(String type, String businessDate, Integer 
statusId, Double principalDisbursed, Double principalOutstanding) {
+        super(type, businessDate);
+        this.statusId = statusId;
+        this.principalDisbursed = principalDisbursed;
+        this.principalOutstanding = principalOutstanding;
+    }
+
+    public LoanBusinessEvent(String type, String businessDate, Integer 
statusId, Double principalDisbursed, Double principalOutstanding,
+            List<String> loanTermVariationType) {
+        super(type, businessDate);
+        this.statusId = statusId;
+        this.principalDisbursed = principalDisbursed;
+        this.principalOutstanding = principalOutstanding;
+        this.loanTermVariationType = loanTermVariationType;
+    }
+
+    @Override
+    public boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter 
formatter) {
+        Object summaryRes = externalEvent.getPayLoad().get("summary");
+        Object statusRes = externalEvent.getPayLoad().get("status");
+        Map<String, Object> summary = summaryRes instanceof Map ? (Map<String, 
Object>) summaryRes : Map.of();
+        Map<String, Object> status = statusRes instanceof Map ? (Map<String, 
Object>) statusRes : Map.of();
+        var principalDisbursed = summary.get("principalDisbursed");
+
+        var principalOutstanding = summary.get("principalOutstanding");
+        Double statusId = (Double) status.get("id");
+        return super.verify(externalEvent, formatter) && 
Objects.equals(statusId, getStatusId().doubleValue())
+                && Objects.equals(principalDisbursed, getPrincipalDisbursed())
+                && Objects.equals(principalOutstanding, 
getPrincipalOutstanding()) && loanTermVariationsMatch(
+                        (List<Map<String, Object>>) 
externalEvent.getPayLoad().get("loanTermVariations"), loanTermVariationType);
+    }
+
+    private boolean loanTermVariationsMatch(final List<Map<String, Object>> 
loanTermVariations, final List<String> expectedTypes) {
+        if (CollectionUtils.isEmpty(expectedTypes)) {
+            return true;
+        }
+        final long numberOfMatches = expectedTypes
+                .stream().filter(
+                        expectedType -> loanTermVariations.stream()
+                                .anyMatch(variation -> StringUtils
+                                        .equals((String) ((Map<String, 
Object>) variation.get("termType")).get("value"), expectedType)))
+                .count();
+
+        return numberOfMatches == expectedTypes.size();
+    }
+}
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/LoanTransactionBusinessEvent.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/LoanTransactionBusinessEvent.java
new file mode 100644
index 0000000000..c2c061d83f
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/externalevents/LoanTransactionBusinessEvent.java
@@ -0,0 +1,63 @@
+/**
+ * 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 java.time.format.DateTimeFormatter;
+import java.util.Objects;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import 
org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class LoanTransactionBusinessEvent extends BusinessEvent {
+
+    private Double amount;
+    private Double outstandingLoanBalance;
+    private Double principalPortion;
+    private Double interestPortion;
+    private Double feeChargesPortion;
+    private Double penaltyChargesPortion;
+
+    public LoanTransactionBusinessEvent(String type, String businessDate, 
Double amount, Double outstandingLoanBalance,
+            Double principalPortion, Double interestPortion, Double 
feeChargesPortion, Double penaltyChargesPortion) {
+        super(type, businessDate);
+        this.amount = amount;
+        this.outstandingLoanBalance = outstandingLoanBalance;
+        this.principalPortion = principalPortion;
+        this.interestPortion = interestPortion;
+        this.feeChargesPortion = feeChargesPortion;
+        this.penaltyChargesPortion = penaltyChargesPortion;
+    }
+
+    @Override
+    public boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter 
formatter) {
+        Object amount = externalEvent.getPayLoad().get("amount");
+        Object outstandingLoanBalance = 
externalEvent.getPayLoad().get("outstandingLoanBalance");
+        Object principalPortion = 
externalEvent.getPayLoad().get("principalPortion");
+        Object interestPortion = 
externalEvent.getPayLoad().get("interestPortion");
+        Object feePortion = 
externalEvent.getPayLoad().get("feeChargesPortion");
+        Object penaltyPortion = 
externalEvent.getPayLoad().get("penaltyChargesPortion");
+
+        return super.verify(externalEvent, formatter) && 
Objects.equals(amount, getAmount())
+                && Objects.equals(outstandingLoanBalance, 
getOutstandingLoanBalance())
+                && Objects.equals(principalPortion, getPrincipalPortion()) && 
Objects.equals(interestPortion, getInterestPortion())
+                && Objects.equals(feePortion, getFeeChargesPortion()) && 
Objects.equals(penaltyPortion, getPenaltyChargesPortion());
+    }
+}

Reply via email to