You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ba...@apache.org on 2024/03/20 15:49:42 UTC

(fineract) branch develop updated: FINERACT-2068: Loan Status Change History

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 c34f2b4f5 FINERACT-2068: Loan Status Change History
c34f2b4f5 is described below

commit c34f2b4f59db708bf99490cb78af52df313e8f5f
Author: Peter Bagrij <pe...@dpc.hu>
AuthorDate: Wed Mar 20 12:22:06 2024 +0100

    FINERACT-2068: Loan Status Change History
---
 .../core/config/FineractProperties.java            |   1 +
 .../domain/LoanStatusChangeHistory.java            |  55 ++++++
 .../domain/LoanStatusChangeHistoryRepository.java  |  25 +++
 .../tenant/module/loan/module-changelog-master.xml |   1 +
 .../parts/1021_add_loan_status_change_history.xml  |  89 +++++++++
 .../service/LoanStatusChangeHistoryListener.java   |  94 +++++++++
 .../src/main/resources/application.properties      |   4 +
 .../LoanStatusChangeHistoryListenerTest.java       | 210 +++++++++++++++++++++
 .../src/test/resources/application-test.properties |   1 +
 9 files changed, 480 insertions(+)

diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java
index 7a5d647c8..beb28d0f9 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java
@@ -449,6 +449,7 @@ public class FineractProperties {
     public static class FineractLoanProperties {
 
         private FineractTransactionProcessorProperties transactionProcessor;
+        private String statusChangeHistoryStatuses;
     }
 
     @Getter
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanStatusChangeHistory.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanStatusChangeHistory.java
new file mode 100644
index 000000000..371a0915e
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanStatusChangeHistory.java
@@ -0,0 +1,55 @@
+/**
+ * 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.portfolio.loanaccount.domain;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.Table;
+import java.time.LocalDate;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
+
+@Entity
+@Table(name = "m_loan_status_change_history")
+@Getter
+@AllArgsConstructor
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class LoanStatusChangeHistory extends AbstractAuditableWithUTCDateTimeCustom {
+
+    @ManyToOne
+    @JoinColumn(name = "loan_id", nullable = false)
+    private Loan loan;
+
+    @Enumerated(EnumType.STRING)
+    @Column(name = "status_code", nullable = false)
+    @Setter
+    private LoanStatus status;
+
+    @Column(name = "status_change_business_date", nullable = false)
+    private LocalDate businessDate;
+
+}
diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanStatusChangeHistoryRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanStatusChangeHistoryRepository.java
new file mode 100644
index 000000000..45560d202
--- /dev/null
+++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanStatusChangeHistoryRepository.java
@@ -0,0 +1,25 @@
+/**
+ * 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.portfolio.loanaccount.domain;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+
+public interface LoanStatusChangeHistoryRepository
+        extends JpaRepository<LoanStatusChangeHistory, Long>, JpaSpecificationExecutor<LoanStatusChangeHistory> {}
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 9655538f0..4fffd9b3e 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
@@ -43,4 +43,5 @@
   <include relativeToChangelogFile="true" file="parts/1018_rename_credited_principal_back_to_credits_amount.xml"/>
   <include relativeToChangelogFile="true" file="parts/1019_add_fixed_length.xml"/>
   <include relativeToChangelogFile="true" file="parts/1020_add_re_aged_flag_to_loan_installment.xml"/>
+  <include relativeToChangelogFile="true" file="parts/1021_add_loan_status_change_history.xml"/>
 </databaseChangeLog>
diff --git a/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1021_add_loan_status_change_history.xml b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1021_add_loan_status_change_history.xml
new file mode 100644
index 000000000..413bcf353
--- /dev/null
+++ b/fineract-loan/src/main/resources/db/changelog/tenant/module/loan/parts/1021_add_loan_status_change_history.xml
@@ -0,0 +1,89 @@
+<?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.3.xsd">
+    <changeSet author="fineract" id="1021-1">
+        <createTable tableName="m_loan_status_change_history">
+            <column autoIncrement="true" name="id" type="BIGINT">
+                <constraints nullable="false" primaryKey="true"/>
+            </column>
+            <column name="loan_id" type="BIGINT">
+                <constraints nullable="false" foreignKeyName="m_loan_status_change_history_fk" referencedTableName="m_loan" referencedColumnNames="id"/>
+            </column>
+            <column name="status_code" type="VARCHAR(255)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="status_change_business_date" type="date">
+                <constraints nullable="false"/>
+            </column>
+            <column name="created_by" type="BIGINT">
+                <constraints nullable="false"/>
+            </column>
+            <column name="last_modified_by" type="BIGINT">
+                <constraints nullable="false"/>
+            </column>
+        </createTable>
+    </changeSet>
+    <changeSet author="fineract" id="1021-2" context="mysql">
+        <addColumn tableName="m_loan_status_change_history">
+            <column name="created_on_utc" type="DATETIME(6)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="last_modified_on_utc" type="DATETIME(6)">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+    </changeSet>
+    <changeSet author="fineract" id="1021-2" context="postgresql">
+        <addColumn tableName="m_loan_status_change_history">
+            <column name="created_on_utc" type="TIMESTAMP WITH TIME ZONE">
+                <constraints nullable="false"/>
+            </column>
+            <column name="last_modified_on_utc" type="TIMESTAMP WITH TIME ZONE">
+                <constraints nullable="false"/>
+            </column>
+        </addColumn>
+    </changeSet>
+    <changeSet author="fineract" id="1021-3">
+        <addForeignKeyConstraint baseColumnNames="created_by" baseTableName="m_loan_status_change_history"
+                                 constraintName="FK_oan_status_change_history_created_by" deferrable="false" initiallyDeferred="false"
+                                 onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
+                                 referencedTableName="m_appuser" validate="true"/>
+        <addForeignKeyConstraint baseColumnNames="last_modified_by" baseTableName="m_loan_status_change_history"
+                                 constraintName="FK_loan_status_change_history_last_modified_by" deferrable="false"
+                                 initiallyDeferred="false"
+                                 onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
+                                 referencedTableName="m_appuser" validate="true"/>
+    </changeSet>
+    <changeSet author="fineract" id="1021-4">
+        <createIndex tableName="m_loan_status_change_history" indexName="IND_loan_status_change_history_loan_id">
+            <column name="loan_id"/>
+        </createIndex>
+        <createIndex tableName="m_loan_status_change_history" indexName="IND_loan_status_change_hist_status_code">
+            <column name="status_code"/>
+        </createIndex>
+        <createIndex tableName="m_loan_status_change_history" indexName="IND_loan_status_change_hist_s_change_bus_date">
+            <column name="status_change_business_date"/>
+        </createIndex>
+    </changeSet>
+</databaseChangeLog>
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangeHistoryListener.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangeHistoryListener.java
new file mode 100644
index 000000000..1048d620a
--- /dev/null
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangeHistoryListener.java
@@ -0,0 +1,94 @@
+/**
+ * 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.portfolio.loanaccount.service;
+
+import com.google.common.base.Splitter;
+import jakarta.annotation.PostConstruct;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
+import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatusChangeHistory;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatusChangeHistoryRepository;
+import org.springframework.stereotype.Service;
+
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class LoanStatusChangeHistoryListener {
+
+    private final Set<LoanStatus> loanStatuses = new HashSet<>();
+
+    private final BusinessEventNotifierService businessEventNotifierService;
+    private final LoanStatusChangeHistoryRepository loanStatusChangeHistoryRepository;
+    private final FineractProperties fineractProperties;
+
+    @PostConstruct
+    public void addListeners() {
+        loanStatuses.addAll(getLoanStatuses(fineractProperties.getLoan().getStatusChangeHistoryStatuses()));
+        if (loanStatuses.size() > 0) {
+            businessEventNotifierService.addPostBusinessEventListener(LoanStatusChangedBusinessEvent.class,
+                    new LoanStatusChangedListener());
+        }
+    }
+
+    Set<LoanStatus> getLoanStatuses(String str) {
+        Set<LoanStatus> result = new HashSet<>();
+        if ("NONE".equals(StringUtils.trim(str))) {
+            return result;
+        } else if ("ALL".equals(StringUtils.trim(str))) {
+            return Arrays.stream(LoanStatus.values()).collect(Collectors.toSet());
+        } else {
+            List<String> split = Splitter.on(",").trimResults().omitEmptyStrings().splitToList(str);
+            for (int i = 0; i < split.size(); i++) {
+                try {
+                    result.add(Enum.valueOf(LoanStatus.class, split.get(i)));
+                } catch (IllegalArgumentException iae) {
+                    throw new RuntimeException("Invalid loan status: " + split.get(i), iae);
+                }
+            }
+        }
+        return result;
+    }
+
+    protected final class LoanStatusChangedListener implements BusinessEventListener<LoanStatusChangedBusinessEvent> {
+
+        @Override
+        public void onBusinessEvent(LoanStatusChangedBusinessEvent event) {
+            final Loan loan = event.get();
+            log.debug("Loan Status change for loan {} with status {}", loan.getId(), loan.getStatus());
+            if (loanStatuses.contains(loan.getStatus())) {
+                LoanStatusChangeHistory loanStatusChangeHistory = new LoanStatusChangeHistory(loan, loan.getStatus(),
+                        DateUtils.getBusinessLocalDate());
+                loanStatusChangeHistoryRepository.saveAndFlush(loanStatusChangeHistory);
+            }
+        }
+    }
+}
diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties
index 77624ba83..a49c6403c 100644
--- a/fineract-provider/src/main/resources/application.properties
+++ b/fineract-provider/src/main/resources/application.properties
@@ -142,6 +142,10 @@ fineract.loan.transactionprocessor.due-penalty-interest-principal-fee-in-advance
 fineract.loan.transactionprocessor.advanced-payment-strategy.enabled=${FINERACT_LOAN_TRANSACTIONPROCESSOR_ADVANCED_PAYMENT_STRATEGY_ENABLED:true}
 fineract.loan.transactionprocessor.error-not-found-fail=${FINERACT_LOAN_TRANSACTIONPROCESSOR_ERROR_NOT_FOUND_FAIL:true}
 
+# Comma separated list of loan statuses which will be recorded on change. There are two extra values: "NONE" and "ALL".
+# "NONE" disables the feature and no entries will be created, "ALL" enables the feature for all loan statuses.
+fineract.loan.status-change-history-statuses=${FINERACT_LOAN_STATUS_CHANGE_HISTORY_STATUSES:NONE}
+
 fineract.content.regex-whitelist-enabled=${FINERACT_CONTENT_REGEX_WHITELIST_ENABLED:true}
 fineract.content.regex-whitelist=${FINERACT_CONTENT_REGEX_WHITELIST:.*\\.pdf$,.*\\.doc,.*\\.docx,.*\\.xls,.*\\.xlsx,.*\\.jpg,.*\\.jpeg,.*\\.png}
 fineract.content.mime-whitelist-enabled=${FINERACT_CONTENT_MIME_WHITELIST_ENABLED:true}
diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangeHistoryListenerTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangeHistoryListenerTest.java
new file mode 100644
index 000000000..43c6e575a
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanStatusChangeHistoryListenerTest.java
@@ -0,0 +1,210 @@
+/**
+ * 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.portfolio.loanaccount.service;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.infrastructure.core.config.FineractProperties;
+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.BusinessEventListener;
+import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatusChangeHistory;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatusChangeHistoryRepository;
+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 LoanStatusChangeHistoryListenerTest {
+
+    @Mock
+    private BusinessEventNotifierService businessEventNotifierService;
+    @Mock
+    private LoanStatusChangeHistoryRepository loanStatusChangeHistoryRepository;
+    @Mock
+    private FineractProperties fineractProperties;
+
+    @Captor
+    private ArgumentCaptor<Class<LoanStatusChangedBusinessEvent>> classArgumentCaptor;
+
+    @Captor
+    private ArgumentCaptor<BusinessEventListener<LoanStatusChangedBusinessEvent>> listenerCaptor;
+
+    @Captor
+    private ArgumentCaptor<LoanStatusChangeHistory> loanStatusChangeHistoryArgumentCaptor;
+
+    @InjectMocks
+    private LoanStatusChangeHistoryListener underTest;
+
+    private final LocalDate actualDate = LocalDate.now(ZoneId.systemDefault());
+
+    @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, actualDate)));
+    }
+
+    @Test
+    public void testGetLoanStatusesParseOK() {
+        Assertions.assertEquals(Set.of(), underTest.getLoanStatuses("NONE"));
+        Assertions.assertEquals(Arrays.stream(LoanStatus.values()).collect(Collectors.toSet()), underTest.getLoanStatuses("ALL"));
+        Assertions.assertEquals(Set.of(LoanStatus.OVERPAID, LoanStatus.REJECTED, LoanStatus.CLOSED_OBLIGATIONS_MET),
+                underTest.getLoanStatuses("OVERPAID,REJECTED,CLOSED_OBLIGATIONS_MET"));
+        Assertions.assertEquals(Set.of(LoanStatus.OVERPAID, LoanStatus.REJECTED, LoanStatus.CLOSED_OBLIGATIONS_MET),
+                underTest.getLoanStatuses(" OVERPAID,  REJECTED ,CLOSED_OBLIGATIONS_MET   "));
+    }
+
+    @Test
+    public void testGetLoanStatusesParseNOK() {
+        RuntimeException runtimeException = Assertions.assertThrows(RuntimeException.class, () -> underTest.getLoanStatuses("MISSING"));
+        Assertions.assertEquals("Invalid loan status: MISSING", runtimeException.getMessage());
+
+        runtimeException = Assertions.assertThrows(RuntimeException.class, () -> underTest.getLoanStatuses("OVERPAID,MISSING"));
+        Assertions.assertEquals("Invalid loan status: MISSING", runtimeException.getMessage());
+
+        runtimeException = Assertions.assertThrows(RuntimeException.class, () -> underTest.getLoanStatuses("ACTIVE,ALL"));
+        Assertions.assertEquals("Invalid loan status: ALL", runtimeException.getMessage());
+
+        runtimeException = Assertions.assertThrows(RuntimeException.class, () -> underTest.getLoanStatuses("APPROVED,NONE"));
+        Assertions.assertEquals("Invalid loan status: NONE", runtimeException.getMessage());
+    }
+
+    @Test
+    public void testEventListenerShouldNotBeRegisteredWhenNONE() {
+        // given
+        FineractProperties.FineractLoanProperties loanProperties = Mockito.mock(FineractProperties.FineractLoanProperties.class);
+        Mockito.when(fineractProperties.getLoan()).thenReturn(loanProperties);
+        Mockito.when(loanProperties.getStatusChangeHistoryStatuses()).thenReturn("NONE");
+
+        // when
+        underTest.addListeners();
+
+        // then
+        Mockito.verifyNoInteractions(businessEventNotifierService);
+    }
+
+    @Test
+    public void testEventListenerShouldBeRegisteredWhenAll() {
+        // given
+        FineractProperties.FineractLoanProperties loanProperties = Mockito.mock(FineractProperties.FineractLoanProperties.class);
+        Mockito.when(fineractProperties.getLoan()).thenReturn(loanProperties);
+        Mockito.when(loanProperties.getStatusChangeHistoryStatuses()).thenReturn("ALL");
+
+        // when
+        underTest.addListeners();
+
+        // then
+        Mockito.verify(businessEventNotifierService, Mockito.times(1)).addPostBusinessEventListener(classArgumentCaptor.capture(),
+                listenerCaptor.capture());
+        Mockito.verifyNoMoreInteractions(businessEventNotifierService);
+        Assertions.assertNotNull(listenerCaptor.getValue());
+    }
+
+    @Test
+    public void testEventListenerShouldBeRegisteredWhenValidStatusesAreProvided() {
+        // given
+        FineractProperties.FineractLoanProperties loanProperties = Mockito.mock(FineractProperties.FineractLoanProperties.class);
+        Mockito.when(fineractProperties.getLoan()).thenReturn(loanProperties);
+        Mockito.when(loanProperties.getStatusChangeHistoryStatuses()).thenReturn("ACTIVE, REJECTED");
+
+        // when
+        underTest.addListeners();
+
+        // then
+        Mockito.verify(businessEventNotifierService, Mockito.times(1)).addPostBusinessEventListener(classArgumentCaptor.capture(),
+                listenerCaptor.capture());
+        Mockito.verifyNoMoreInteractions(businessEventNotifierService);
+        Assertions.assertNotNull(listenerCaptor.getValue());
+    }
+
+    @Test
+    public void testHistoryIsSavedWhenLoansStateIsConfigured() {
+        // given
+        FineractProperties.FineractLoanProperties loanProperties = Mockito.mock(FineractProperties.FineractLoanProperties.class);
+        Mockito.when(fineractProperties.getLoan()).thenReturn(loanProperties);
+        Mockito.when(loanProperties.getStatusChangeHistoryStatuses()).thenReturn("ACTIVE, REJECTED");
+        underTest.addListeners();
+        Mockito.verify(businessEventNotifierService, Mockito.times(1)).addPostBusinessEventListener(classArgumentCaptor.capture(),
+                listenerCaptor.capture());
+        Mockito.verifyNoMoreInteractions(businessEventNotifierService);
+        BusinessEventListener<LoanStatusChangedBusinessEvent> listener = listenerCaptor.getValue();
+
+        LoanStatusChangedBusinessEvent mockEvent = Mockito.mock(LoanStatusChangedBusinessEvent.class);
+        Loan loan = Mockito.mock(Loan.class);
+        Mockito.when(loan.getId()).thenReturn(123L);
+        Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+        Mockito.when(mockEvent.get()).thenReturn(loan);
+
+        // when
+        listener.onBusinessEvent(mockEvent);
+
+        // then
+        Mockito.verify(loanStatusChangeHistoryRepository, Mockito.times(1)).saveAndFlush(loanStatusChangeHistoryArgumentCaptor.capture());
+        Mockito.verifyNoMoreInteractions(loanStatusChangeHistoryRepository);
+        LoanStatusChangeHistory loanStatusChangeHistory = loanStatusChangeHistoryArgumentCaptor.getValue();
+        Assertions.assertEquals(loan, loanStatusChangeHistory.getLoan());
+        Assertions.assertEquals(LoanStatus.ACTIVE, loanStatusChangeHistory.getStatus());
+        Assertions.assertEquals(actualDate, loanStatusChangeHistory.getBusinessDate());
+    }
+
+    @Test
+    public void testHistoryIsNotSavedWhenLoansStateIsNotConfigured() {
+        // given
+        FineractProperties.FineractLoanProperties loanProperties = Mockito.mock(FineractProperties.FineractLoanProperties.class);
+        Mockito.when(fineractProperties.getLoan()).thenReturn(loanProperties);
+        Mockito.when(loanProperties.getStatusChangeHistoryStatuses()).thenReturn("ACTIVE, REJECTED");
+        underTest.addListeners();
+        Mockito.verify(businessEventNotifierService, Mockito.times(1)).addPostBusinessEventListener(classArgumentCaptor.capture(),
+                listenerCaptor.capture());
+        Mockito.verifyNoMoreInteractions(businessEventNotifierService);
+        BusinessEventListener<LoanStatusChangedBusinessEvent> listener = listenerCaptor.getValue();
+
+        LoanStatusChangedBusinessEvent mockEvent = Mockito.mock(LoanStatusChangedBusinessEvent.class);
+        Loan loan = Mockito.mock(Loan.class);
+        Mockito.when(loan.getId()).thenReturn(123L);
+        Mockito.when(loan.getStatus()).thenReturn(LoanStatus.OVERPAID);
+        Mockito.when(mockEvent.get()).thenReturn(loan);
+
+        // when
+        listener.onBusinessEvent(mockEvent);
+
+        // then
+        Mockito.verifyNoInteractions(loanStatusChangeHistoryRepository);
+    }
+
+}
diff --git a/fineract-provider/src/test/resources/application-test.properties b/fineract-provider/src/test/resources/application-test.properties
index 36c7231ae..9f9eb43ce 100644
--- a/fineract-provider/src/test/resources/application-test.properties
+++ b/fineract-provider/src/test/resources/application-test.properties
@@ -73,6 +73,7 @@ fineract.loan.transactionprocessor.due-penalty-fee-interest-principal-in-advance
 fineract.loan.transactionprocessor.due-penalty-interest-principal-fee-in-advance-penalty-interest-principal-fee.enabled=true
 fineract.loan.transactionprocessor.advanced-payment-strategy.enabled=true
 fineract.loan.transactionprocessor.error-not-found-fail=true
+fineract.loan.status-change-history-statuses=NONE
 
 fineract.content.regex-whitelist-enabled=true
 fineract.content.regex-whitelist=.*\\.pdf$,.*\\.doc,.*\\.docx,.*\\.xls,.*\\.xlsx,.*\\.jpg,.*\\.jpeg,.*\\.png