diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java
index e2ab1715a6f..82d91b4989d 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java
@@ -75,6 +75,7 @@ public final class GlobalConfigurationConstants {
public static final String ENABLE_SAME_MAKER_CHECKER = "enable-same-maker-checker";
public static final String NEXT_PAYMENT_DUE_DATE = "next-payment-due-date";
public static final String ENABLE_PAYMENT_HUB_INTEGRATION = "enable-payment-hub-integration";
+ public static final String ENABLE_IMMEDIATE_CHARGE_ACCRUAL_POST_MATURITY = "enable-immediate-charge-accrual-post-maturity";
private GlobalConfigurationConstants() {}
}
diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
index 164986e8715..e7482d16a1d 100644
--- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
+++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
@@ -143,4 +143,6 @@ public interface ConfigurationDomainService {
String getNextPaymentDateConfigForLoan();
+ boolean isImmediateChargeAccrualPostMaturityEnabled();
+
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java
index 386fc6635bf..2e8558184a4 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java
@@ -523,4 +523,8 @@ public String getNextPaymentDateConfigForLoan() {
return value;
}
+ @Override
+ public boolean isImmediateChargeAccrualPostMaturityEnabled() {
+ return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.ENABLE_IMMEDIATE_CHARGE_ACCRUAL_POST_MATURITY).isEnabled();
+ }
}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
index 75f86c50a25..ed020371815 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java
@@ -55,6 +55,7 @@
import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanUpdateChargeBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanWaiveChargeBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanWaiveChargeUndoBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeAdjustmentPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeAdjustmentPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeRefundBusinessEvent;
@@ -1033,9 +1034,13 @@ private boolean addCharge(final Loan loan, final Charge chargeDefinition, LoanCh
// we want to apply charge transactions only for those loans charges that are applied when a loan is active and
// the loan product uses Upfront Accruals, or only when the loan are closed too,
if ((loan.getStatus().isActive() && loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct())
- || loan.getStatus().isOverpaid() || loan.getStatus().isClosedObligationsMet()) {
+ || loan.getStatus().isOverpaid() || loan.getStatus().isClosedObligationsMet()
+ || (configurationDomainService.isImmediateChargeAccrualPostMaturityEnabled()
+ && DateUtils.getBusinessLocalDate().isAfter(loan.getMaturityDate()))) {
final LoanTransaction applyLoanChargeTransaction = loan.handleChargeAppliedTransaction(loanCharge, null);
this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction);
+ businessEventNotifierService
+ .notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(applyLoanChargeTransaction));
}
return DateUtils.isBeforeBusinessDate(loanCharge.getDueLocalDate());
}
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 340df505e94..af723c51ae2 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
@@ -173,4 +173,5 @@
+
diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0155_add_configuration_enable_immediate_charge_accrual_post_maturity.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0155_add_configuration_enable_immediate_charge_accrual_post_maturity.xml
new file mode 100644
index 00000000000..3080942d9d8
--- /dev/null
+++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0155_add_configuration_enable_immediate_charge_accrual_post_maturity.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+ SELECT SETVAL('c_configuration_id_seq', COALESCE(MAX(id), 0)+1, false ) FROM c_configuration;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImplTest.java
new file mode 100644
index 00000000000..f2d74fbb991
--- /dev/null
+++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImplTest.java
@@ -0,0 +1,169 @@
+/**
+ * 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 static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.time.LocalDate;
+import java.util.stream.Stream;
+import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
+import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
+import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.portfolio.charge.domain.Charge;
+import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
+import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
+import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
+import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+class LoanChargeWritePlatformServiceImplTest {
+
+ private static final Long LOAN_ID = 1L;
+ private static final Integer SPECIFIED_DUE_DATE = 2;
+ private static final LocalDate MATURITY_DATE = LocalDate.of(2024, 2, 15);
+ private static final LocalDate BUSINESS_DATE_AFTER = LocalDate.of(2024, 2, 26);
+ private static final LocalDate BUSINESS_DATE_ON = MATURITY_DATE;
+ private static final LocalDate BUSINESS_DATE_BEFORE = LocalDate.of(2024, 2, 14);
+
+ @InjectMocks
+ private LoanChargeWritePlatformServiceImpl loanChargeWritePlatformService;
+
+ @Mock
+ private JsonCommand jsonCommand;
+
+ @Mock
+ private LoanChargeApiJsonValidator loanChargeApiJsonValidator;
+
+ @Mock
+ private LoanAssembler loanAssembler;
+
+ @Mock
+ private Loan loan;
+
+ @Mock
+ private ChargeRepositoryWrapper chargeRepository;
+
+ @Mock
+ private Charge chargeDefinition;
+
+ @Mock
+ private LoanChargeAssembler loanChargeAssembler;
+
+ @Mock
+ private LoanCharge loanCharge;
+
+ @Mock
+ private BusinessEventNotifierService businessEventNotifierService;
+
+ @Mock
+ private LoanProductRelatedDetail loanRepaymentScheduleDetail;
+
+ @Mock
+ private LoanChargeRepository loanChargeRepository;
+
+ @Mock
+ private ConfigurationDomainService configurationDomainService;
+
+ @Mock
+ private LoanTransactionRepository loanTransactionRepository;
+
+ @Mock
+ private LoanTransaction loanTransaction;
+
+ @Mock
+ private LoanAccountDomainService loanAccountDomainService;
+
+ @Mock
+ private MonetaryCurrency monetaryCurrency;
+
+ @Mock
+ private JournalEntryWritePlatformService journalEntryWritePlatformService;
+
+ @BeforeEach
+ void setUp() {
+ when(loanAssembler.assembleFrom(LOAN_ID)).thenReturn(loan);
+ when(chargeRepository.findOneWithNotFoundDetection(anyLong())).thenReturn(chargeDefinition);
+ when(chargeDefinition.getChargeTimeType()).thenReturn(SPECIFIED_DUE_DATE);
+ when(loanChargeAssembler.createNewFromJson(loan, chargeDefinition, jsonCommand)).thenReturn(loanCharge);
+ when(loan.repaymentScheduleDetail()).thenReturn(loanRepaymentScheduleDetail);
+ when(loan.hasCurrencyCodeOf(any())).thenReturn(true);
+ when(loanCharge.getChargePaymentMode()).thenReturn(ChargePaymentMode.REGULAR);
+ when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+ when(loanChargeRepository.saveAndFlush(any(LoanCharge.class))).thenReturn(loanCharge);
+ when(loan.getCurrency()).thenReturn(monetaryCurrency);
+ when(loanAccountDomainService.saveAndFlushLoanWithDataIntegrityViolationChecks(any())).thenReturn(loan);
+ }
+
+ @ParameterizedTest
+ @MethodSource("loanChargeAccrualTestCases")
+ void shouldHandleAccrualBasedOnConfigurationAndDates(boolean isAccrualEnabled, LocalDate businessDate, LocalDate maturityDate, boolean isAccrualExpected) {
+ when(configurationDomainService.isImmediateChargeAccrualPostMaturityEnabled()).thenReturn(isAccrualEnabled);
+ when(loan.getMaturityDate()).thenReturn(maturityDate);
+ when(loan.handleChargeAppliedTransaction(loanCharge, null)).thenReturn(loanTransaction);
+
+ try (MockedStatic mockedDateUtils = mockStatic(DateUtils.class)) {
+ mockedDateUtils.when(DateUtils::getBusinessLocalDate).thenReturn(businessDate);
+
+ loanChargeWritePlatformService.addLoanCharge(LOAN_ID, jsonCommand);
+ }
+
+ if (isAccrualExpected) {
+ verify(loanTransactionRepository, times(1)).saveAndFlush(any(LoanTransaction.class));
+ verify(businessEventNotifierService, times(1)).notifyPostBusinessEvent(any(LoanAccrualTransactionCreatedBusinessEvent.class));
+ } else {
+ verify(loanTransactionRepository, never()).saveAndFlush(any(LoanTransaction.class));
+ verify(businessEventNotifierService, never()).notifyPostBusinessEvent(any(LoanAccrualTransactionCreatedBusinessEvent.class));
+ }
+ }
+
+ private static Stream loanChargeAccrualTestCases() {
+ return Stream.of(Arguments.of(true, BUSINESS_DATE_AFTER, MATURITY_DATE, true),
+ Arguments.of(false, BUSINESS_DATE_AFTER, MATURITY_DATE, false), Arguments.of(true, BUSINESS_DATE_ON, MATURITY_DATE, false),
+ Arguments.of(true, BUSINESS_DATE_BEFORE, MATURITY_DATE, false));
+ }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java
index 51f6fb0ae3d..2e88b496784 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java
@@ -100,8 +100,8 @@ public void verifyAllDefaultGlobalConfigurations() {
ArrayList expectedGlobalConfigurations = getAllDefaultGlobalConfigurations();
GetGlobalConfigurationsResponse actualGlobalConfigurations = getAllGlobalConfigurations();
- Assertions.assertEquals(55, expectedGlobalConfigurations.size());
- Assertions.assertEquals(55, actualGlobalConfigurations.getGlobalConfiguration().size());
+ Assertions.assertEquals(56, expectedGlobalConfigurations.size());
+ Assertions.assertEquals(56, actualGlobalConfigurations.getGlobalConfiguration().size());
for (int i = 0; i < expectedGlobalConfigurations.size(); i++) {
@@ -528,6 +528,13 @@ private static ArrayList getAllDefaultGlobalConfigurations() {
enablePaymentHubIntegrationConfig.put("string_value", "enable payment hub integration");
defaults.add(enablePaymentHubIntegrationConfig);
+ HashMap enableImmediateChargeAccrualPostMaturity = new HashMap<>();
+ enableImmediateChargeAccrualPostMaturity.put("name", GlobalConfigurationConstants.ENABLE_IMMEDIATE_CHARGE_ACCRUAL_POST_MATURITY);
+ enableImmediateChargeAccrualPostMaturity.put("value", 0L);
+ enableImmediateChargeAccrualPostMaturity.put("enabled", false);
+ enableImmediateChargeAccrualPostMaturity.put("trapDoor", false);
+ defaults.add(enableImmediateChargeAccrualPostMaturity);
+
return defaults;
}