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 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() {}
|
||||
}
|
||||
|
||||
+2
@@ -143,4 +143,6 @@ public interface ConfigurationDomainService {
|
||||
|
||||
String getNextPaymentDateConfigForLoan();
|
||||
|
||||
boolean isImmediateChargeAccrualPostMaturityEnabled();
|
||||
|
||||
}
|
||||
|
||||
+4
@@ -523,4 +523,8 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
|
||||
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.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 @@ 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
|
||||
// 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());
|
||||
}
|
||||
|
||||
@@ -173,4 +173,5 @@
|
||||
<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/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>
|
||||
|
||||
+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();
|
||||
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 @@ public class GlobalConfigurationHelper extends IntegrationTest {
|
||||
enablePaymentHubIntegrationConfig.put("string_value", "enable payment hub integration");
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user