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..df53007436a 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 @@ -18,6 +18,8 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +import static org.apache.fineract.infrastructure.core.service.DateUtils.getBusinessLocalDate; + import com.google.gson.JsonElement; import com.google.gson.JsonObject; import java.math.BigDecimal; @@ -55,6 +57,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; @@ -301,7 +304,7 @@ public CommandProcessingResult addLoanCharge(final Long loanId, final JsonComman loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); } - this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); + this.loanAccountDomainService.setLoanDelinquencyTag(loan, getBusinessLocalDate()); businessEventNotifierService.notifyPostBusinessEvent(new LoanAddChargeBusinessEvent(loanCharge)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); @@ -542,7 +545,7 @@ public CommandProcessingResult waiveLoanCharge(final Long loanId, final Long loa externalId); if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled() - && DateUtils.isBefore(loanCharge.getDueLocalDate(), DateUtils.getBusinessLocalDate())) { + && DateUtils.isBefore(loanCharge.getDueLocalDate(), getBusinessLocalDate())) { loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); @@ -552,7 +555,7 @@ public CommandProcessingResult waiveLoanCharge(final Long loanId, final Long loa this.loanRepositoryWrapper.save(loan); postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); - this.loanAccountDomainService.setLoanDelinquencyTag(loan, DateUtils.getBusinessLocalDate()); + this.loanAccountDomainService.setLoanDelinquencyTag(loan, getBusinessLocalDate()); loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); businessEventNotifierService.notifyPostBusinessEvent(new LoanWaiveChargeBusinessEvent(loanCharge)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); @@ -702,7 +705,7 @@ public CommandProcessingResult adjustmentForLoanCharge(Long loanId, Long loanCha this.loanChargeApiJsonValidator.validateLoanChargeAdjustmentRequest(loanId, loanChargeId, command.json()); final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); - final LocalDate transactionDate = DateUtils.getBusinessLocalDate(); + final LocalDate transactionDate = getBusinessLocalDate(); final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("amount"); final ExternalId externalId = externalIdFactory.createFromCommand(command, "externalId"); final String locale = command.locale(); @@ -766,7 +769,7 @@ public void applyOverdueChargesForLoan(final Long loanId, Collection existingTransactionIds = loan.findExistingTransactionIds(); final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); boolean runInterestRecalculation = false; - LocalDate recalculateFrom = DateUtils.getBusinessLocalDate(); + LocalDate recalculateFrom = getBusinessLocalDate(); LocalDate lastChargeDate = null; for (final OverdueLoanScheduleData overdueInstallment : overdueLoanScheduleDataList) { @@ -821,7 +824,7 @@ public void applyOverdueChargesForLoan(final Long loanId, Collection + 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..66da3a6a8c6 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImplTest.java @@ -0,0 +1,164 @@ +/** + * 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 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.*; +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; + +import java.time.LocalDate; +import java.util.stream.Stream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; + +@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) + ); + } +} \ No newline at end of file