Skip to content

Commit ba37c25

Browse files
authored
Merge pull request #5787
FINERACT-2421: EMI Rounding issue due to 0.01 disbursement
2 parents 3cfc364 + 65bb5ae commit ba37c25

5 files changed

Lines changed: 153 additions & 0 deletions

File tree

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public enum DefaultLoanProduct implements LoanProduct {
4444
LP2_DOWNPAYMENT, //
4545
LP2_DOWNPAYMENT_AUTO, //
4646
LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION, //
47+
LP2_DOWNPAYMENT_AUTO_ADV_PMT_ALLOC_NO_MULTIPLES_OF, //
4748
LP2_DOWNPAYMENT_AUTO_ADVANCED_CUSTOM_PAYMENT_ALLOCATION, //
4849
LP2_DOWNPAYMENT_ADVANCED_PAYMENT_ALLOCATION, //
4950
LP2_DOWNPAYMENT_INTEREST, //

fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public abstract class TestContextKey {
8383
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_INTEREST_DECLINING_PERIOD_DAILY_ACCRUAL_ACTIVITY = "loanProductCreateResponseLP1InterestDecliningPeriodDailyAccrualActivity";
8484
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP1_INTEREST_DECLINING_BALANCE_DAILY_RECALCULATION_COMPOUNDING_NONE_ACCRUAL_ACTIVITY = "loanProductCreateResponseLP1InterestDecliningBalanceRecalculationCompoundingNoneAccrualActivity";
8585
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION = "loanProductCreateResponseLP2DownPaymentAutoAdvancedPaymentAllocation";
86+
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_AUTO_ADV_PMT_ALLOC_NO_MULTIPLES_OF = "loanProductCreateResponseLP2DownPaymentAutoAdvPmtAllocNoMultiplesOf";
8687
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_AUTO_ADVANCED_CUSTOM_PAYMENT_ALLOCATION = "loanProductCreateResponseLP2DownPaymentAutoAdvancedCustomPaymentAllocation";
8788
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_RECALC_EMI_360_30_MULTIDISB_CAPITALIZED_INCOME_ADJ_CUSTOM_ALLOC = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyMultidisbursalCapitalizedIncomeAdjCustomAlloc";
8889
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_AUTO_ADVANCED_REPAYMENT_ALLOCATION_PAYMENT_START_SUBMITTED = "loanProductCreateResponseLP2DownPaymentAutoAdvancedPaymentAllocationPaymentStartSubmitted";

fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,26 @@ public void initialize() throws Exception {
335335
TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION,
336336
responseLoanProductsRequestDownPaymentAutoAdvPaymentAllocation);
337337

338+
// LP2 with Down-payment+autopayment + advanced payment allocation, no installmentAmountInMultiplesOf
339+
// (LP2_DOWNPAYMENT_AUTO_ADV_PMT_ALLOC_NO_MULTIPLES_OF)
340+
final String nameDownPaymentAutoAdvPmtAllocNoMultiplesOf = DefaultLoanProduct.LP2_DOWNPAYMENT_AUTO_ADV_PMT_ALLOC_NO_MULTIPLES_OF
341+
.getName();
342+
final PostLoanProductsRequest loanProductsRequestDownPaymentAutoAdvPmtAllocNoMultiplesOf = loanProductsRequestFactory
343+
.defaultLoanProductsRequestLP2()//
344+
.name(nameDownPaymentAutoAdvPmtAllocNoMultiplesOf)//
345+
.installmentAmountInMultiplesOf(null)//
346+
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION.getValue())//
347+
.loanScheduleType("PROGRESSIVE")//
348+
.paymentAllocation(List.of(//
349+
createPaymentAllocation("DEFAULT", "REAMORTIZATION"), //
350+
createPaymentAllocation("GOODWILL_CREDIT", "NEXT_INSTALLMENT"), //
351+
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "LAST_INSTALLMENT"), //
352+
createPaymentAllocation("PAYOUT_REFUND", "LAST_INSTALLMENT")));//
353+
final PostLoanProductsResponse responseLoanProductsRequestDownPaymentAutoAdvPmtAllocNoMultiplesOf = createLoanProductIdempotent(
354+
loanProductsRequestDownPaymentAutoAdvPmtAllocNoMultiplesOf);
355+
TestContext.INSTANCE.set(TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_AUTO_ADV_PMT_ALLOC_NO_MULTIPLES_OF,
356+
responseLoanProductsRequestDownPaymentAutoAdvPmtAllocNoMultiplesOf);
357+
338358
// LP2 with Down-payment + advanced payment allocation - no auto downpayment
339359
// (LP2_DOWNPAYMENT_ADVANCED_PAYMENT_ALLOCATION)
340360
String name24 = DefaultLoanProduct.LP2_DOWNPAYMENT_ADVANCED_PAYMENT_ALLOCATION.getName();

fineract-e2e-tests-runner/src/test/resources/features/EMICalculation-Part4.feature

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,3 +447,130 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan
447447
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
448448
| 01 January 2024 | Disbursement | 1000.0 | 0.0 | 0.0 | 0.0 | 0.0 | 1000.0 |
449449

450+
@TestRailId:C76742
451+
Scenario: Progressive loan with downpayment - 0.01 disbursement is assigned to the last installment and full repayment closes the loan
452+
When Admin sets the business date to "18 February 2026"
453+
When Admin creates a client with random data
454+
When Admin creates a fully customized loan with the following data:
455+
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
456+
| LP2_DOWNPAYMENT_AUTO_ADV_PMT_ALLOC_NO_MULTIPLES_OF | 18 February 2026 | 100 | 0 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 48 | DAYS | 16 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
457+
And Admin successfully approves the loan on "18 February 2026" with "100" amount and expected disbursement date on "18 February 2026"
458+
When Admin successfully disburse the loan on "18 February 2026" with "0.01" EUR transaction amount
459+
Then Loan status will be "ACTIVE"
460+
Then Loan Repayment schedule has 4 periods, with the following data for periods:
461+
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
462+
| | | 18 February 2026 | | 0.01 | | | 0.0 | | 0.0 | 0.0 | | | |
463+
| 1 | 0 | 18 February 2026 | 18 February 2026 | 0.01 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
464+
| 2 | 16 | 06 March 2026 | 18 February 2026 | 0.01 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
465+
| 3 | 16 | 22 March 2026 | 18 February 2026 | 0.01 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
466+
| 4 | 16 | 07 April 2026 | | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | 0.01 |
467+
Then Loan Repayment schedule has the following data in Total row:
468+
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
469+
| 0.01 | 0.0 | 0.0 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | 0.01 |
470+
Then Loan has 0.01 outstanding amount
471+
When Admin sets the business date to "08 April 2026"
472+
And Customer makes "AUTOPAY" repayment on "08 April 2026" with 0.01 EUR transaction amount
473+
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
474+
Then Loan has 0.0 outstanding amount
475+
Then Loan Transactions tab has the following data:
476+
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
477+
| 18 February 2026 | Disbursement | 0.01 | 0.0 | 0.0 | 0.0 | 0.0 | 0.01 |
478+
| 08 April 2026 | Repayment | 0.01 | 0.01 | 0.0 | 0.0 | 0.0 | 0.0 |
479+
480+
@TestRailId:C76743
481+
Scenario: Disbursement of 0.01 on progressive product distributes residual to first installment
482+
When Admin sets the business date to "18 February 2026"
483+
When Admin creates a client with random data
484+
When Admin creates a fully customized loan with the following data:
485+
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
486+
| LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 18 February 2026 | 100 | 0 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 48 | DAYS | 16 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
487+
And Admin successfully approves the loan on "18 February 2026" with "100" amount and expected disbursement date on "18 February 2026"
488+
When Admin successfully disburse the loan on "18 February 2026" with "0.01" EUR transaction amount
489+
Then Loan status will be "ACTIVE"
490+
Then Loan has 0.01 outstanding amount
491+
Then Loan Repayment schedule has 4 periods, with the following data for periods:
492+
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
493+
| | | 18 February 2026 | | 0.01 | | | 0.0 | | 0.0 | 0.0 | | | |
494+
| 1 | 0 | 18 February 2026 | 18 February 2026 | 0.01 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
495+
| 2 | 16 | 06 March 2026 | | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | 0.01 |
496+
| 3 | 16 | 22 March 2026 | 18 February 2026 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
497+
| 4 | 16 | 07 April 2026 | 18 February 2026 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
498+
Then Loan Repayment schedule has the following data in Total row:
499+
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
500+
| 0.01 | 0.0 | 0.0 | 0.0 | 0.01 | 0.0 | 0.0 | 0.0 | 0.01 |
501+
When Admin sets the business date to "06 March 2026"
502+
And Customer makes "AUTOPAY" repayment on "06 March 2026" with 0.01 EUR transaction amount
503+
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
504+
Then Loan has 0.0 outstanding amount
505+
Then Loan Transactions tab has the following data:
506+
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
507+
| 18 February 2026 | Disbursement | 0.01 | 0.0 | 0.0 | 0.0 | 0.0 | 0.01 |
508+
| 06 March 2026 | Repayment | 0.01 | 0.01 | 0.0 | 0.0 | 0.0 | 0.0 |
509+
510+
@TestRailId:C76744
511+
Scenario: Disbursement of 0.02 on progressive product distributes residual to first installment
512+
When Admin sets the business date to "18 February 2026"
513+
When Admin creates a client with random data
514+
When Admin creates a fully customized loan with the following data:
515+
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
516+
| LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 18 February 2026 | 100 | 0 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 48 | DAYS | 16 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
517+
And Admin successfully approves the loan on "18 February 2026" with "100" amount and expected disbursement date on "18 February 2026"
518+
When Admin successfully disburse the loan on "18 February 2026" with "0.02" EUR transaction amount
519+
Then Loan status will be "ACTIVE"
520+
Then Loan has 0.02 outstanding amount
521+
Then Loan Repayment schedule has 4 periods, with the following data for periods:
522+
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
523+
| | | 18 February 2026 | | 0.02 | | | 0.0 | | 0.0 | 0.0 | | | |
524+
| 1 | 0 | 18 February 2026 | 18 February 2026 | 0.02 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
525+
| 2 | 16 | 06 March 2026 | | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | 0.02 |
526+
| 3 | 16 | 22 March 2026 | 18 February 2026 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
527+
| 4 | 16 | 07 April 2026 | 18 February 2026 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 | 0.0 |
528+
Then Loan Repayment schedule has the following data in Total row:
529+
| Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
530+
| 0.02 | 0.0 | 0.0 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | 0.02 |
531+
When Admin sets the business date to "06 March 2026"
532+
And Customer makes "AUTOPAY" repayment on "06 March 2026" with 0.02 EUR transaction amount
533+
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
534+
Then Loan has 0.0 outstanding amount
535+
Then Loan Transactions tab has the following data:
536+
| Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
537+
| 18 February 2026 | Disbursement | 0.02 | 0.0 | 0.0 | 0.0 | 0.0 | 0.02 |
538+
| 06 March 2026 | Repayment | 0.02 | 0.02 | 0.0 | 0.0 | 0.0 | 0.0 |
539+
540+
@TestRailId:C76745
541+
Scenario: Partial repayment of 0.01 residual does not flip loan to OVERPAID
542+
When Admin sets the business date to "18 February 2026"
543+
When Admin creates a client with random data
544+
When Admin creates a fully customized loan with the following data:
545+
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
546+
| LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 18 February 2026 | 100 | 0 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 48 | DAYS | 16 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
547+
And Admin successfully approves the loan on "18 February 2026" with "100" amount and expected disbursement date on "18 February 2026"
548+
When Admin successfully disburse the loan on "18 February 2026" with "0.01" EUR transaction amount
549+
Then Loan status will be "ACTIVE"
550+
# Repayment of 0.01 on the disbursement day must close the loan, NOT leave balance 0.01 and flip to OVERPAID
551+
And Customer makes "AUTOPAY" repayment on "18 February 2026" with 0.01 EUR transaction amount
552+
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
553+
Then Loan has 0.0 outstanding amount
554+
555+
@TestRailId:C76746
556+
Scenario: Multi-disbursement, 0.01 first tranche followed by 99.99 second tranche amortises cleanly
557+
When Admin sets the business date to "18 February 2026"
558+
When Admin creates a client with random data
559+
When Admin creates a fully customized loan with the following data:
560+
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
561+
| LP2_DOWNPAYMENT_AUTO_ADVANCED_PAYMENT_ALLOCATION | 18 February 2026 | 100 | 0 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 48 | DAYS | 16 | DAYS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
562+
And Admin successfully approves the loan on "18 February 2026" with "100" amount and expected disbursement date on "18 February 2026"
563+
# First tranche
564+
When Admin successfully disburse the loan on "18 February 2026" with "0.01" EUR transaction amount
565+
Then Loan status will be "ACTIVE"
566+
Then Loan has 0.01 outstanding amount
567+
# Second tranche on the same day, before any installment is due, must re-amortise to a full 100-loan schedule.
568+
# Auto-downpayment on 99.99 is round(99.99 * 25%, multiplesOf=1) = 25, so outstanding = 0.01 + 99.99 - 25 = 75.00.
569+
When Admin successfully disburse the loan on "18 February 2026" with "99.99" EUR transaction amount
570+
Then Loan status will be "ACTIVE"
571+
Then Loan has 75.0 outstanding amount
572+
# Pay down full outstanding, loan must close cleanly (not OVERPAID with a ghost 0.01 residual from the first tranche)
573+
When Admin sets the business date to "08 April 2026"
574+
And Customer makes "AUTOPAY" repayment on "08 April 2026" with 75 EUR transaction amount
575+
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
576+
Then Loan has 0.0 outstanding amount

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,10 @@ private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestSchedu
11301130

11311131
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
11321132
.reduce((first, second) -> second);
1133+
if (findLastUnpaidRepaymentPeriod.isEmpty() && scheduleModel.getTotalPaidPrincipal().isZero()) {
1134+
findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().reduce((first, second) -> second)
1135+
.filter(rp -> !rp.getInterestPeriods().isEmpty()).filter(rp -> rp.getOutstandingLoanBalance().isGreaterThanZero());
1136+
}
11331137

11341138
findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> {
11351139
repaymentPeriod.setFutureUnrecognizedInterest(scheduleModel.zero());

0 commit comments

Comments
 (0)