FINERACT-2114: show interest rate change in business events

This commit is contained in:
adam.magyari
2024-09-17 13:52:01 +02:00
committed by Adam Saghy
parent 77b8eeef95
commit 1cb9a36865
5 changed files with 135 additions and 27 deletions
@@ -20,6 +20,7 @@ package org.apache.fineract.infrastructure.event.external.service.serialization.
import java.util.Collection;
import java.util.List;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.avro.generic.GenericContainer;
import org.apache.commons.collections4.CollectionUtils;
@@ -37,6 +38,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData;
import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryBalancesRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
import org.springframework.stereotype.Component;
@@ -84,6 +86,11 @@ public class LoanBusinessEventSerializer implements BusinessEventSerializer {
List<LoanInstallmentDelinquencyBucketDataV1> installmentsDelinquencyData = installmentLevelDelinquencyEventProducer
.calculateInstallmentLevelDelinquencyData(event.get(), data.getCurrency());
Set<LoanTermVariations> activeLoanTermVariations = event.get().getActiveLoanTermVariations();
if (activeLoanTermVariations != null) {
data.setEmiAmountVariations(activeLoanTermVariations.stream().map(LoanTermVariations::toData).toList());
}
LoanAccountDataV1 result = mapper.map(data);
result.getDelinquent().setInstallmentDelinquencyBuckets(installmentsDelinquencyData);
return result;
@@ -77,7 +77,6 @@ import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.EarlyPaymentLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor;
@@ -4427,8 +4426,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(5.0).interestCalculationPeriodType(DAYS).interestRateFrequencyType(YEARS)
.daysInMonthType(DaysInMonthType.DAYS_30.getValue()).daysInYearType(DaysInYearType.DAYS_360.getValue())
.numberOfRepayments(5)//
.daysInMonthType(DaysInMonthType.DAYS_30.getValue()).daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(5)//
.repaymentEvery(1)//
.repaymentFrequencyType(2L)//
.enableDownPayment(true)//
@@ -4490,8 +4488,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(12.3).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).interestRateFrequencyType(YEARS)
.daysInMonthType(DaysInMonthType.DAYS_30.getValue()).daysInYearType(DaysInYearType.DAYS_360.getValue())
.numberOfRepayments(5)//
.daysInMonthType(DaysInMonthType.DAYS_30.getValue()).daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(5)//
.repaymentEvery(1)//
.repaymentFrequencyType(2L)//
.enableDownPayment(true)//
@@ -4554,8 +4551,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(12.3).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).interestRateFrequencyType(YEARS)
.daysInMonthType(DaysInMonthType.DAYS_30.getValue()).daysInYearType(DaysInYearType.DAYS_360.getValue())
.numberOfRepayments(5)//
.daysInMonthType(DaysInMonthType.DAYS_30.getValue()).daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(5)//
.repaymentEvery(1)//
.repaymentFrequencyType(2L)//
.enableDownPayment(true)//
@@ -4618,8 +4614,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(12.3).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).interestRateFrequencyType(YEARS)
.daysInMonthType(DaysInMonthType.ACTUAL.getValue()).daysInYearType(DaysInYearType.DAYS_365.getValue())
.numberOfRepayments(4)//
.daysInMonthType(DaysInMonthType.ACTUAL.getValue()).daysInYearType(DaysInYearType.DAYS_365).numberOfRepayments(4)//
.repaymentEvery(1)//
.repaymentFrequencyType(2L)//
.allowPartialPeriodInterestCalcualtion(false)//
@@ -4698,7 +4693,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(108.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL.getValue())
.daysInYearType(DaysInYearType.DAYS_360.getValue()).numberOfRepayments(4)//
.daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(4)//
.maxInterestRatePerPeriod((double) 110)//
.repaymentEvery(1)//
.repaymentFrequencyType(1L)//
@@ -4878,8 +4873,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
interestRefundTypes.add("MERCHANT_ISSUED_REFUND");
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(12.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).interestRateFrequencyType(YEARS)
.daysInMonthType(DaysInMonthType.ACTUAL.getValue()).daysInYearType(DaysInYearType.DAYS_365.getValue())
.numberOfRepayments(4)//
.daysInMonthType(DaysInMonthType.ACTUAL.getValue()).daysInYearType(DaysInYearType.DAYS_365).numberOfRepayments(4)//
.repaymentEvery(5)//
.repaymentFrequencyType(0L)//
.allowPartialPeriodInterestCalcualtion(false)//
@@ -4942,7 +4936,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(interestRatePerPeriod.doubleValue()).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.DAYS_30.getValue())
.daysInYearType(DaysInYearType.DAYS_360.getValue()).numberOfRepayments(6)//
.daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(6)//
.repaymentEvery(1)//
.repaymentFrequencyType(2L)//
.enableDownPayment(false)//
@@ -5024,7 +5018,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(interestRatePerPeriod.doubleValue()).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.DAYS_30.getValue())
.daysInYearType(DaysInYearType.DAYS_360.getValue()).numberOfRepayments(6)//
.daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(6)//
.repaymentEvery(1)//
.repaymentFrequencyType(2L)//
.enableDownPayment(false)//
@@ -5121,7 +5115,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
.interestCalculationPeriodType(RepaymentFrequencyType.DAYS)//
.interestRateFrequencyType(YEARS)//
.daysInMonthType(DaysInMonthType.DAYS_30.getValue())//
.daysInYearType(DaysInYearType.DAYS_360.getValue())//
.daysInYearType(DaysInYearType.DAYS_360)//
.numberOfRepayments(6)//
.repaymentEvery(1)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())//
@@ -1046,6 +1046,7 @@ public abstract class BaseLoanIntegrationTest {
public static class InterestCalculationPeriodType {
public static final Integer DAILY = 0;
public static final Integer SAME_AS_REPAYMENT_PERIOD = 1;
}
@@ -1055,4 +1056,12 @@ public abstract class BaseLoanIntegrationTest {
public static final Integer YEARS = 3;
}
public static class DaysInYearType {
public static final Integer INVALID = 0;
public static final Integer ACTUAL = 1;
public static final Integer DAYS_360 = 360;
public static final Integer DAYS_364 = 364;
public static final Integer DAYS_365 = 365;
}
}
@@ -36,13 +36,19 @@ import java.util.concurrent.atomic.AtomicReference;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest;
import org.apache.fineract.client.models.PostCreateRescheduleLoansResponse;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest;
import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
import org.apache.fineract.integrationtests.common.BusinessStepHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.integrationtests.common.ExternalEventConfigurationHelper;
import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
import org.apache.fineract.integrationtests.common.Utils;
import org.apache.fineract.integrationtests.common.externalevents.ExternalEventHelper;
import org.apache.fineract.integrationtests.common.externalevents.ExternalEventsExtension;
@@ -61,6 +67,7 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
private static final String DATETIME_PATTERN = "dd MMMM yyyy";
private static PostClientsResponse client;
private static LoanTransactionHelper loanTransactionHelper;
private static LoanRescheduleRequestHelper loanRescheduleRequestHelper;
private static Long loanProductId;
private static ResponseSpecification responseSpec;
private static RequestSpecification requestSpec;
@@ -74,6 +81,7 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec);
loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
loanRescheduleRequestHelper = new LoanRescheduleRequestHelper(requestSpec, responseSpec);
BusinessStepHelper businessStepHelper = new BusinessStepHelper();
client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
loanProductId = createLoanProductPeriodicWithInterest();
@@ -141,6 +149,33 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
});
}
@Test
public void testExternalBusinessEventLoanRescheduledDueAdjustScheduleBusinessEventInterestChange() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("01 March 2024", () -> {
enableLoanRescheduledDueAdjustScheduleBusinessEvent();
Long loanId = applyForLoanApplicationWithInterest(client.getClientId(), loanProductId, BigDecimal.valueOf(4000), "1 March 2023",
"1 March 2024");
loanIdRef.set(loanId);
loanTransactionHelper.approveLoan("1 March 2024", loanId.intValue());
loanTransactionHelper.disburseLoan("1 March 2024", loanId.intValue(), "400", null);
PostCreateRescheduleLoansResponse rescheduleLoansResponse = loanRescheduleRequestHelper
.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanIdRef.get()).dateFormat(DATETIME_PATTERN)
.locale("en").submittedOnDate("1 March 2024").newInterestRate(BigDecimal.ONE).rescheduleReasonId(1L)
.rescheduleFromDate("1 April 2024"));
deleteAllExternalEvents();
loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleLoansResponse.getResourceId(),
new PostUpdateRescheduleLoansRequest().approvedOnDate("1 March 2024").locale("en").dateFormat(DATETIME_PATTERN));
verifyBusinessEvents(new BusinessEvent("LoanRescheduledDueAdjustScheduleBusinessEvent", "01 March 2024", 300, 400.0, 400.0,
List.of("interestRateForInstallment")));
});
}
private static void enableLoanBalanceChangedBusinessEvent() {
final Map<String, Boolean> updatedConfigurations = ExternalEventConfigurationHelper.updateExternalEventConfigurations(requestSpec,
responseSpec, "{\"externalEventConfigurations\":{\"LoanBalanceChangedBusinessEvent\":true}}\n");
@@ -149,6 +184,14 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
Assertions.assertTrue(updatedConfigurations.get("LoanBalanceChangedBusinessEvent"));
}
private static void enableLoanRescheduledDueAdjustScheduleBusinessEvent() {
final Map<String, Boolean> updatedConfigurations = ExternalEventConfigurationHelper.updateExternalEventConfigurations(requestSpec,
responseSpec, "{\"externalEventConfigurations\":{\"LoanRescheduledDueAdjustScheduleBusinessEvent\":true}}\n");
Assertions.assertEquals(updatedConfigurations.size(), 1);
Assertions.assertTrue(updatedConfigurations.containsKey("LoanRescheduledDueAdjustScheduleBusinessEvent"));
Assertions.assertTrue(updatedConfigurations.get("LoanRescheduledDueAdjustScheduleBusinessEvent"));
}
private static void disableLoanBalanceChangedBusinessEvent() {
final Map<String, Boolean> updatedConfigurations = ExternalEventConfigurationHelper.updateExternalEventConfigurations(requestSpec,
responseSpec, "{\"externalEventConfigurations\":{\"LoanBalanceChangedBusinessEvent\":false}}\n");
@@ -166,14 +209,38 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
private static Long createLoanProductPeriodicWithInterest() {
String name = Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6);
String shortName = Utils.uniqueRandomStringGenerator("", 4);
Long resourceId = loanTransactionHelper.createLoanProduct(new PostLoanProductsRequest().name(name).shortName(shortName)
.multiDisburseLoan(true).maxTrancheCount(2).interestType(0).interestCalculationPeriodType(0)
.disallowExpectedDisbursements(true).description("Test loan description").currencyCode("USD").digitsAfterDecimal(2)
.daysInYearType(1).daysInMonthType(1).interestRecalculationCompoundingMethod(0).recalculationRestFrequencyType(1)
.rescheduleStrategyMethod(1).recalculationRestFrequencyInterval(0).isInterestRecalculationEnabled(false)
.interestRateFrequencyType(2).locale("en_GB").numberOfRepayments(4).repaymentFrequencyType(2L).interestRatePerPeriod(2.0)
.repaymentEvery(1).minPrincipal(100.0).principal(1000.0).maxPrincipal(10000000.0).amortizationType(1)
.dateFormat(DATETIME_PATTERN).transactionProcessingStrategyCode(DEFAULT_STRATEGY).accountingRule(1)).getResourceId();
Long resourceId = loanTransactionHelper.createLoanProduct(new PostLoanProductsRequest() //
.name(name) //
.shortName(shortName) //
.multiDisburseLoan(true) //
.maxTrancheCount(2) //
.interestType(InterestType.DECLINING_BALANCE) //
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY) //
.disallowExpectedDisbursements(true) //
.description("Test loan description") //
.currencyCode("USD") //
.digitsAfterDecimal(2) //
.daysInYearType(DaysInYearType.ACTUAL) //
.daysInMonthType(DaysInYearType.ACTUAL) //
.interestRecalculationCompoundingMethod(0) //
.recalculationRestFrequencyType(1) //
.rescheduleStrategyMethod(1) //
.recalculationRestFrequencyInterval(0) //
.isInterestRecalculationEnabled(false) //
.interestRateFrequencyType(2) //
.locale("en_GB") //
.numberOfRepayments(4) //
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) //
.interestRatePerPeriod(2.0) //
.repaymentEvery(1) //
.minPrincipal(100.0) //
.principal(1000.0) //
.maxPrincipal(10000000.0) //
.amortizationType(AmortizationType.EQUAL_INSTALLMENTS) //
.dateFormat(DATETIME_PATTERN) //
.transactionProcessingStrategyCode(DEFAULT_STRATEGY) //
.accountingRule(1)) //
.getResourceId();
log.info("Test MultiDisburse Loan Product With Interest. loanProductId: {}", resourceId);
return resourceId;
}
@@ -222,20 +289,52 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
&& Objects.equals(externalEvent.getBusinessDate(), businessDate)
&& Objects.equals(statusId, businessEvent.getStatusId().doubleValue())
&& Objects.equals(principalDisbursed, businessEvent.getPrincipalDisbursed())
&& Objects.equals(principalOutstanding, businessEvent.getPrincipalOutstanding());
&& Objects.equals(principalOutstanding, businessEvent.getPrincipalOutstanding())
&& emiAmountVariationsMatch((List<Map<String, Object>>) externalEvent.getPayLoad().get("emiAmountVariations"),
businessEvent.emiAmountVariationType);
}).count();
Assertions.assertEquals(1, count, "Expected business event not found " + businessEvent);
}
}
private boolean emiAmountVariationsMatch(List<Map<String, Object>> emiAmountVariations, List<String> expectedTypes) {
if (CollectionUtils.isEmpty(expectedTypes)) {
return true;
}
int numberOfMatches = 0;
if (CollectionUtils.isNotEmpty(emiAmountVariations)) {
for (String expectedType : expectedTypes) {
int i = 0;
while (i < emiAmountVariations.size() && !StringUtils
.equals((String) ((Map<String, Object>) emiAmountVariations.get(i).get("termType")).get("value"), expectedType)) {
i++;
}
if (i < emiAmountVariations.size()) {
numberOfMatches++;
}
}
}
return numberOfMatches == expectedTypes.size();
}
@Data
@AllArgsConstructor
public static class BusinessEvent {
public BusinessEvent(String type, String businessDate, Integer statusId, Double principalDisbursed, Double principalOutstanding) {
this.type = type;
this.businessDate = businessDate;
this.statusId = statusId;
this.principalDisbursed = principalDisbursed;
this.principalOutstanding = principalOutstanding;
}
private String type;
private String businessDate;
private Integer statusId;
private Double principalDisbursed;
private Double principalOutstanding;
private List<String> emiAmountVariationType;
}
}
@@ -45,7 +45,6 @@ import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
@@ -91,7 +90,7 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest {
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(108.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL.getValue())
.daysInYearType(DaysInYearType.DAYS_360.getValue()).numberOfRepayments(4)//
.daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(4)//
.maxInterestRatePerPeriod((double) 110)//
.repaymentEvery(1)//
.repaymentFrequencyType(1L)//
@@ -163,7 +162,7 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest {
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
.interestRatePerPeriod(108.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL.getValue())
.daysInYearType(DaysInYearType.DAYS_360.getValue()).numberOfRepayments(4)//
.daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(4)//
.maxInterestRatePerPeriod((double) 110)//
.repaymentEvery(1)//
.repaymentFrequencyType(1L)//