Skip to content

Commit c9c9e9f

Browse files
committed
FINERACT-2326: Loan point in time API now properly handles future dates
1 parent ccfe062 commit c9c9e9f

File tree

3 files changed

+124
-7
lines changed

3 files changed

+124
-7
lines changed

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanPointInTimeServiceImpl.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,24 @@ public LoanPointInTimeData retrieveAt(Long loanId, LocalDate date) {
6565
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, date)));
6666

6767
Loan loan = loanAssembler.assembleFrom(loanId);
68+
69+
int txCount = loan.getLoanTransactions().size();
70+
int chargeCount = loan.getCharges().size();
6871
removeAfterDateTransactions(loan, date);
6972
removeAfterDateCharges(loan, date);
73+
int afterRemovalTxCount = loan.getLoanTransactions().size();
74+
int afterRemovalChargeCount = loan.getCharges().size();
7075

71-
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
72-
loanScheduleService.recalculateSchedule(loan, scheduleGeneratorDTO);
76+
// In case the loan is cumulative and is being prepaid by the latest repayment tx, we need the
77+
// recalculateFrom and recalculateTill
78+
// set to the same date which is the prepaying transaction's date
79+
// currently this is not implemented and opens up buggy edge cases
80+
// we work this around only for cases when the loan is already closed or the requested date doesn't change
81+
// the loan's state
82+
if (txCount != afterRemovalTxCount || chargeCount != afterRemovalChargeCount) {
83+
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
84+
loanScheduleService.recalculateSchedule(loan, scheduleGeneratorDTO);
85+
}
7386

7487
LoanArrearsData arrearsData = arrearsAgingService.calculateArrearsForLoan(loan);
7588

integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -992,7 +992,7 @@ protected void verifyTransactions(final Long loanId, final TransactionExt... tra
992992
}
993993
}
994994

995-
protected void verifyArreals(LoanPointInTimeData pointInTimeData, boolean isOverDue, String overdueSince) {
995+
protected void verifyArrears(LoanPointInTimeData pointInTimeData, boolean isOverDue, String overdueSince) {
996996
assertThat(Objects.requireNonNull(pointInTimeData.getArrears()).getOverdue()).isEqualTo(isOverDue);
997997
if (isOverDue) {
998998
assertThat(Objects.requireNonNull(pointInTimeData.getArrears().getOverDueSince()).toString()).isEqualTo(overdueSince);

integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/pointintime/LoanPointInTimeTest.java

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,7 @@ public void test_LoansPointInTimeDataWorks_ForPrincipalOutstandingCalculation()
658658
}
659659

660660
@Test
661-
public void test_LoanPointInTimeDataWorks_ForArrealDataCalculation() {
661+
public void test_LoanPointInTimeDataWorks_ForArrearsDataCalculation() {
662662
AtomicReference<Long> aLoanId = new AtomicReference<>();
663663

664664
runAt("01 January 2023", () -> {
@@ -683,7 +683,6 @@ public void test_LoanPointInTimeDataWorks_ForArrealDataCalculation() {
683683
.interestType(InterestType.DECLINING_BALANCE)//
684684
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
685685
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
686-
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
687686
.isInterestRecalculationEnabled(true)//
688687
.recalculationRestFrequencyInterval(1)//
689688
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
@@ -737,14 +736,14 @@ public void test_LoanPointInTimeDataWorks_ForArrealDataCalculation() {
737736

738737
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "10 February 2023");
739738
verifyOutstanding(pointInTimeData, outstanding(5000.0, 0.0, 0.0, 0.0, 5000.0));
740-
verifyArreals(pointInTimeData, true, "2023-02-01");
739+
verifyArrears(pointInTimeData, true, "2023-02-01");
741740

742741
// repay 500
743742
addRepaymentForLoan(loanId, 2500.0, "01 February 2023");
744743

745744
LoanPointInTimeData pointInTimeDataAfterRepay = getPointInTimeData(loanId, "10 February 2023");
746745
verifyOutstanding(pointInTimeDataAfterRepay, outstanding(2500.0, 0.0, 0.0, 0.0, 2500.0));
747-
verifyArreals(pointInTimeDataAfterRepay, false, null);
746+
verifyArrears(pointInTimeDataAfterRepay, false, null);
748747

749748
// verify transactions
750749
verifyTransactions(loanId, //
@@ -754,4 +753,109 @@ public void test_LoanPointInTimeDataWorks_ForArrealDataCalculation() {
754753
);
755754
});
756755
}
756+
757+
@Test
758+
public void test_LoanPointInTimeDataWorks_ForArrearsDataCalculation_ForFutureDate_WithInterest() {
759+
AtomicReference<Long> aLoanId = new AtomicReference<>();
760+
761+
runAt("01 January 2023", () -> {
762+
// Create Client
763+
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
764+
765+
int numberOfRepayments = 3;
766+
int repaymentEvery = 1;
767+
768+
// Create charges
769+
double charge1Amount = 1.0;
770+
double charge2Amount = 1.5;
771+
Long charge1Id = createDisbursementPercentageCharge(charge1Amount);
772+
Long charge2Id = createDisbursementPercentageCharge(charge2Amount);
773+
774+
// Create Loan Product
775+
double interestRatePerPeriod = 10.0;
776+
PostLoanProductsRequest product = createOnePeriod30DaysPeriodicAccrualProduct(interestRatePerPeriod) //
777+
.numberOfRepayments(numberOfRepayments) //
778+
.repaymentEvery(repaymentEvery) //
779+
.installmentAmountInMultiplesOf(null) //
780+
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) //
781+
.interestType(InterestType.DECLINING_BALANCE)//
782+
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
783+
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
784+
.isInterestRecalculationEnabled(true)//
785+
.recalculationRestFrequencyInterval(1)//
786+
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)//
787+
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
788+
.allowPartialPeriodInterestCalcualtion(false)//
789+
.disallowExpectedDisbursements(false)//
790+
.allowApprovedDisbursedAmountsOverApplied(false)//
791+
.overAppliedNumber(null)//
792+
.overAppliedCalculationType(null)//
793+
.multiDisburseLoan(null)//
794+
.charges(List.of(new LoanProductChargeData().id(charge1Id), new LoanProductChargeData().id(charge2Id)));//
795+
796+
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
797+
Long loanProductId = loanProductResponse.getResourceId();
798+
799+
// Apply and Approve Loan
800+
double amount = 5000.0;
801+
802+
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
803+
.repaymentEvery(repaymentEvery)//
804+
.interestRatePerPeriod(BigDecimal.valueOf(interestRatePerPeriod)).loanTermFrequency(numberOfRepayments)//
805+
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
806+
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
807+
.interestType(InterestType.DECLINING_BALANCE)//
808+
.interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)//
809+
.charges(List.of(//
810+
new PostLoansRequestChargeData().chargeId(charge1Id).amount(BigDecimal.valueOf(charge1Amount)), //
811+
new PostLoansRequestChargeData().chargeId(charge2Id).amount(BigDecimal.valueOf(charge2Amount))//
812+
));//
813+
814+
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
815+
816+
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
817+
approveLoanRequest(amount, "01 January 2023"));
818+
819+
aLoanId.getAndSet(approvedLoanResult.getLoanId());
820+
Long loanId = aLoanId.get();
821+
822+
// disburse Loan
823+
disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January 2023");
824+
825+
// verify transactions
826+
verifyTransactions(loanId, //
827+
transaction(5000.0, "Disbursement", "01 January 2023"), //
828+
transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023") //
829+
);
830+
});
831+
832+
runAt("05 March 2023", () -> {
833+
Long loanId = aLoanId.get();
834+
835+
// repay
836+
addRepaymentForLoan(loanId, 5897.89, "05 March 2023");
837+
838+
// verify transactions
839+
verifyTransactions(loanId, //
840+
transaction(5000.0, "Disbursement", "01 January 2023"), //
841+
transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023"), //
842+
transaction(5897.89, "Repayment", "05 March 2023"), //
843+
transaction(897.89, "Accrual", "05 March 2023") //
844+
);
845+
});
846+
847+
runAt("05 June 2023", () -> {
848+
Long loanId = aLoanId.get();
849+
850+
LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "05 June 2023");
851+
852+
verifyOutstanding(pointInTimeData, outstanding(0.0, 0.0, 0.0, 0.0, 0.0));
853+
verifyArrears(pointInTimeData, false, null);
854+
assertThat(pointInTimeData.getArrears().getPrincipalOverdue()).isZero();
855+
assertThat(pointInTimeData.getArrears().getFeeOverdue()).isZero();
856+
assertThat(pointInTimeData.getArrears().getInterestOverdue()).isZero();
857+
assertThat(pointInTimeData.getArrears().getPenaltyOverdue()).isZero();
858+
assertThat(pointInTimeData.getArrears().getTotalOverdue()).isZero();
859+
});
860+
}
757861
}

0 commit comments

Comments
 (0)