FINERACT-2153: Enable immediate charge accrual creation post maturity
This commit is contained in:
+1
@@ -75,6 +75,7 @@ public final class GlobalConfigurationConstants {
|
|||||||
public static final String ENABLE_SAME_MAKER_CHECKER = "enable-same-maker-checker";
|
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 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_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() {}
|
private GlobalConfigurationConstants() {}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -143,4 +143,6 @@ public interface ConfigurationDomainService {
|
|||||||
|
|
||||||
String getNextPaymentDateConfigForLoan();
|
String getNextPaymentDateConfigForLoan();
|
||||||
|
|
||||||
|
boolean isImmediateChargeAccrualPostMaturityEnabled();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -523,4 +523,8 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isImmediateChargeAccrualPostMaturityEnabled() {
|
||||||
|
return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.ENABLE_IMMEDIATE_CHARGE_ACCRUAL_POST_MATURITY).isEnabled();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -55,6 +55,7 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.charge.Loan
|
|||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanUpdateChargeBusinessEvent;
|
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.LoanWaiveChargeBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.charge.LoanWaiveChargeUndoBusinessEvent;
|
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.LoanChargeAdjustmentPostBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeAdjustmentPreBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeAdjustmentPreBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeRefundBusinessEvent;
|
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanChargeRefundBusinessEvent;
|
||||||
@@ -1033,9 +1034,13 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
|
|||||||
// we want to apply charge transactions only for those loans charges that are applied when a loan is active and
|
// 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,
|
// the loan product uses Upfront Accruals, or only when the loan are closed too,
|
||||||
if ((loan.getStatus().isActive() && loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct())
|
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);
|
final LoanTransaction applyLoanChargeTransaction = loan.handleChargeAppliedTransaction(loanCharge, null);
|
||||||
this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction);
|
this.loanTransactionRepository.saveAndFlush(applyLoanChargeTransaction);
|
||||||
|
businessEventNotifierService
|
||||||
|
.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(applyLoanChargeTransaction));
|
||||||
}
|
}
|
||||||
return DateUtils.isBeforeBusinessDate(loanCharge.getDueLocalDate());
|
return DateUtils.isBeforeBusinessDate(loanCharge.getDueLocalDate());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,4 +173,5 @@
|
|||||||
<include file="parts/0152_update_password_validation_policy.xml" relativeToChangelogFile="true" />
|
<include file="parts/0152_update_password_validation_policy.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0153_add_charge_off_reason_id_to_acc_product_mapping.xml" relativeToChangelogFile="true" />
|
<include file="parts/0153_add_charge_off_reason_id_to_acc_product_mapping.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0154_add_interest_refund_to_r_enum_value.xml" relativeToChangelogFile="true" />
|
<include file="parts/0154_add_interest_refund_to_r_enum_value.xml" relativeToChangelogFile="true" />
|
||||||
|
<include file="parts/0155_add_configuration_enable_immediate_charge_accrual_post_maturity.xml" relativeToChangelogFile="true" />
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
<?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="1" context="postgresql">
|
||||||
|
<sql>
|
||||||
|
SELECT SETVAL('c_configuration_id_seq', COALESCE(MAX(id), 0)+1, false ) FROM c_configuration;
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
<changeSet author="fineract" id="2">
|
||||||
|
<insert tableName="c_configuration">
|
||||||
|
<column name="name" value="enable-immediate-charge-accrual-post-maturity"/>
|
||||||
|
<column name="value"/>
|
||||||
|
<column name="date_value"/>
|
||||||
|
<column name="string_value"/>
|
||||||
|
<column name="enabled" valueBoolean="false"/>
|
||||||
|
<column name="is_trap_door" valueBoolean="false"/>
|
||||||
|
<column name="description" value="Whether the system creates accruals immediately for charge creation after the maturity date"/>
|
||||||
|
</insert>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+169
@@ -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<DateUtils> 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<Arguments> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-2
@@ -100,8 +100,8 @@ public class GlobalConfigurationHelper extends IntegrationTest {
|
|||||||
ArrayList<HashMap> expectedGlobalConfigurations = getAllDefaultGlobalConfigurations();
|
ArrayList<HashMap> expectedGlobalConfigurations = getAllDefaultGlobalConfigurations();
|
||||||
GetGlobalConfigurationsResponse actualGlobalConfigurations = getAllGlobalConfigurations();
|
GetGlobalConfigurationsResponse actualGlobalConfigurations = getAllGlobalConfigurations();
|
||||||
|
|
||||||
Assertions.assertEquals(55, expectedGlobalConfigurations.size());
|
Assertions.assertEquals(56, expectedGlobalConfigurations.size());
|
||||||
Assertions.assertEquals(55, actualGlobalConfigurations.getGlobalConfiguration().size());
|
Assertions.assertEquals(56, actualGlobalConfigurations.getGlobalConfiguration().size());
|
||||||
|
|
||||||
for (int i = 0; i < expectedGlobalConfigurations.size(); i++) {
|
for (int i = 0; i < expectedGlobalConfigurations.size(); i++) {
|
||||||
|
|
||||||
@@ -528,6 +528,13 @@ public class GlobalConfigurationHelper extends IntegrationTest {
|
|||||||
enablePaymentHubIntegrationConfig.put("string_value", "enable payment hub integration");
|
enablePaymentHubIntegrationConfig.put("string_value", "enable payment hub integration");
|
||||||
defaults.add(enablePaymentHubIntegrationConfig);
|
defaults.add(enablePaymentHubIntegrationConfig);
|
||||||
|
|
||||||
|
HashMap<String, Object> 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;
|
return defaults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user