From 32cc83fdbfe45b991d383892eb14ed904f3e5c5e Mon Sep 17 00:00:00 2001 From: leksinomi Date: Tue, 26 Nov 2024 13:31:30 +0200 Subject: [PATCH] FINERACT-2153: Enable immediate charge accrual creation post maturity --- .../api/GlobalConfigurationConstants.java | 1 + .../domain/ConfigurationDomainService.java | 2 + .../domain/ConfigurationDomainServiceJpa.java | 4 + .../LoanChargeWritePlatformServiceImpl.java | 7 +- .../db/changelog/tenant/changelog-tenant.xml | 1 + ...immediate_charge_accrual_post_maturity.xml | 41 +++++ ...oanChargeWritePlatformServiceImplTest.java | 169 ++++++++++++++++++ .../common/GlobalConfigurationHelper.java | 11 +- 8 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0155_add_configuration_enable_immediate_charge_accrual_post_maturity.xml create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImplTest.java 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; }