This is an automated email from the ASF dual-hosted git repository.
arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new c9c9e9f835 FINERACT-2326: Loan point in time API now properly handles
future dates
c9c9e9f835 is described below
commit c9c9e9f835b481663f1912653c91bde8cfbe56ed
Author: Arnold Galovics <[email protected]>
AuthorDate: Tue Oct 7 20:12:12 2025 +0200
FINERACT-2326: Loan point in time API now properly handles future dates
---
.../service/LoanPointInTimeServiceImpl.java | 17 +++-
.../integrationtests/BaseLoanIntegrationTest.java | 2 +-
.../loan/pointintime/LoanPointInTimeTest.java | 112 ++++++++++++++++++++-
3 files changed, 124 insertions(+), 7 deletions(-)
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanPointInTimeServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanPointInTimeServiceImpl.java
index 278c2a9a7d..da5c8db76a 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanPointInTimeServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanPointInTimeServiceImpl.java
@@ -65,11 +65,24 @@ public class LoanPointInTimeServiceImpl implements
LoanPointInTimeService {
ThreadLocalContextUtil.setBusinessDates(new
HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, date)));
Loan loan = loanAssembler.assembleFrom(loanId);
+
+ int txCount = loan.getLoanTransactions().size();
+ int chargeCount = loan.getCharges().size();
removeAfterDateTransactions(loan, date);
removeAfterDateCharges(loan, date);
+ int afterRemovalTxCount = loan.getLoanTransactions().size();
+ int afterRemovalChargeCount = loan.getCharges().size();
- ScheduleGeneratorDTO scheduleGeneratorDTO =
loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
- loanScheduleService.recalculateSchedule(loan,
scheduleGeneratorDTO);
+ // In case the loan is cumulative and is being prepaid by the
latest repayment tx, we need the
+ // recalculateFrom and recalculateTill
+ // set to the same date which is the prepaying transaction's date
+ // currently this is not implemented and opens up buggy edge cases
+ // we work this around only for cases when the loan is already
closed or the requested date doesn't change
+ // the loan's state
+ if (txCount != afterRemovalTxCount || chargeCount !=
afterRemovalChargeCount) {
+ ScheduleGeneratorDTO scheduleGeneratorDTO =
loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
+ loanScheduleService.recalculateSchedule(loan,
scheduleGeneratorDTO);
+ }
LoanArrearsData arrearsData =
arrearsAgingService.calculateArrearsForLoan(loan);
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index df8cc9d843..ba07af2d60 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -992,7 +992,7 @@ public abstract class BaseLoanIntegrationTest extends
IntegrationTest {
}
}
- protected void verifyArreals(LoanPointInTimeData pointInTimeData, boolean
isOverDue, String overdueSince) {
+ protected void verifyArrears(LoanPointInTimeData pointInTimeData, boolean
isOverDue, String overdueSince) {
assertThat(Objects.requireNonNull(pointInTimeData.getArrears()).getOverdue()).isEqualTo(isOverDue);
if (isOverDue) {
assertThat(Objects.requireNonNull(pointInTimeData.getArrears().getOverDueSince()).toString()).isEqualTo(overdueSince);
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/pointintime/LoanPointInTimeTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/pointintime/LoanPointInTimeTest.java
index 969a870322..81705b52d7 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/pointintime/LoanPointInTimeTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/pointintime/LoanPointInTimeTest.java
@@ -658,7 +658,7 @@ public class LoanPointInTimeTest extends
BaseLoanIntegrationTest {
}
@Test
- public void test_LoanPointInTimeDataWorks_ForArrealDataCalculation() {
+ public void test_LoanPointInTimeDataWorks_ForArrearsDataCalculation() {
AtomicReference<Long> aLoanId = new AtomicReference<>();
runAt("01 January 2023", () -> {
@@ -683,7 +683,6 @@ public class LoanPointInTimeTest extends
BaseLoanIntegrationTest {
.interestType(InterestType.DECLINING_BALANCE)//
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
-
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
.isInterestRecalculationEnabled(true)//
.recalculationRestFrequencyInterval(1)//
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
@@ -737,14 +736,14 @@ public class LoanPointInTimeTest extends
BaseLoanIntegrationTest {
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId,
"10 February 2023");
verifyOutstanding(pointInTimeData, outstanding(5000.0, 0.0, 0.0,
0.0, 5000.0));
- verifyArreals(pointInTimeData, true, "2023-02-01");
+ verifyArrears(pointInTimeData, true, "2023-02-01");
// repay 500
addRepaymentForLoan(loanId, 2500.0, "01 February 2023");
LoanPointInTimeData pointInTimeDataAfterRepay =
getPointInTimeData(loanId, "10 February 2023");
verifyOutstanding(pointInTimeDataAfterRepay, outstanding(2500.0,
0.0, 0.0, 0.0, 2500.0));
- verifyArreals(pointInTimeDataAfterRepay, false, null);
+ verifyArrears(pointInTimeDataAfterRepay, false, null);
// verify transactions
verifyTransactions(loanId, //
@@ -754,4 +753,109 @@ public class LoanPointInTimeTest extends
BaseLoanIntegrationTest {
);
});
}
+
+ @Test
+ public void
test_LoanPointInTimeDataWorks_ForArrearsDataCalculation_ForFutureDate_WithInterest()
{
+ AtomicReference<Long> aLoanId = new AtomicReference<>();
+
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ int numberOfRepayments = 3;
+ int repaymentEvery = 1;
+
+ // Create charges
+ double charge1Amount = 1.0;
+ double charge2Amount = 1.5;
+ Long charge1Id = createDisbursementPercentageCharge(charge1Amount);
+ Long charge2Id = createDisbursementPercentageCharge(charge2Amount);
+
+ // Create Loan Product
+ double interestRatePerPeriod = 10.0;
+ PostLoanProductsRequest product =
createOnePeriod30DaysPeriodicAccrualProduct(interestRatePerPeriod) //
+ .numberOfRepayments(numberOfRepayments) //
+ .repaymentEvery(repaymentEvery) //
+ .installmentAmountInMultiplesOf(null) //
+
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) //
+ .interestType(InterestType.DECLINING_BALANCE)//
+
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
+
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
+ .isInterestRecalculationEnabled(true)//
+ .recalculationRestFrequencyInterval(1)//
+
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
+
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
+ .allowPartialPeriodInterestCalcualtion(false)//
+ .disallowExpectedDisbursements(false)//
+ .allowApprovedDisbursedAmountsOverApplied(false)//
+ .overAppliedNumber(null)//
+ .overAppliedCalculationType(null)//
+ .multiDisburseLoan(null)//
+ .charges(List.of(new
LoanProductChargeData().id(charge1Id), new
LoanProductChargeData().id(charge2Id)));//
+
+ PostLoanProductsResponse loanProductResponse =
loanProductHelper.createLoanProduct(product);
+ Long loanProductId = loanProductResponse.getResourceId();
+
+ // Apply and Approve Loan
+ double amount = 5000.0;
+
+ PostLoansRequest applicationRequest = applyLoanRequest(clientId,
loanProductId, "01 January 2023", amount, numberOfRepayments)//
+ .repaymentEvery(repaymentEvery)//
+
.interestRatePerPeriod(BigDecimal.valueOf(interestRatePerPeriod)).loanTermFrequency(numberOfRepayments)//
+ .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
+ .loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
+ .interestType(InterestType.DECLINING_BALANCE)//
+
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
+ .charges(List.of(//
+ new
PostLoansRequestChargeData().chargeId(charge1Id).amount(BigDecimal.valueOf(charge1Amount)),
//
+ new
PostLoansRequestChargeData().chargeId(charge2Id).amount(BigDecimal.valueOf(charge2Amount))//
+ ));//
+
+ PostLoansResponse postLoansResponse =
loanTransactionHelper.applyLoan(applicationRequest);
+
+ PostLoansLoanIdResponse approvedLoanResult =
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+ approveLoanRequest(amount, "01 January 2023"));
+
+ aLoanId.getAndSet(approvedLoanResult.getLoanId());
+ Long loanId = aLoanId.get();
+
+ // disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January
2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(5000.0, "Disbursement", "01 January 2023"), //
+ transaction(125.0, "Repayment (at time of disbursement)",
"01 January 2023") //
+ );
+ });
+
+ runAt("05 March 2023", () -> {
+ Long loanId = aLoanId.get();
+
+ // repay
+ addRepaymentForLoan(loanId, 5897.89, "05 March 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(5000.0, "Disbursement", "01 January 2023"), //
+ transaction(125.0, "Repayment (at time of disbursement)",
"01 January 2023"), //
+ transaction(5897.89, "Repayment", "05 March 2023"), //
+ transaction(897.89, "Accrual", "05 March 2023") //
+ );
+ });
+
+ runAt("05 June 2023", () -> {
+ Long loanId = aLoanId.get();
+
+ LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId,
"05 June 2023");
+
+ verifyOutstanding(pointInTimeData, outstanding(0.0, 0.0, 0.0, 0.0,
0.0));
+ verifyArrears(pointInTimeData, false, null);
+
assertThat(pointInTimeData.getArrears().getPrincipalOverdue()).isZero();
+ assertThat(pointInTimeData.getArrears().getFeeOverdue()).isZero();
+
assertThat(pointInTimeData.getArrears().getInterestOverdue()).isZero();
+
assertThat(pointInTimeData.getArrears().getPenaltyOverdue()).isZero();
+
assertThat(pointInTimeData.getArrears().getTotalOverdue()).isZero();
+ });
+ }
}