This is an automated email from the ASF dual-hosted git repository.
adamsaghy 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 fc93db16b5 FINERACT-2319: Add penalty-first payment allocation
strategy for progressive loans with backdated repayment charge handling
fc93db16b5 is described below
commit fc93db16b59ebb84a1844266a94b99f61bf87088
Author: Oleksii Novikov <[email protected]>
AuthorDate: Mon Jun 30 14:13:43 2025 +0300
FINERACT-2319: Add penalty-first payment allocation strategy for
progressive loans with backdated repayment charge handling
---
.../test/data/loanproduct/DefaultLoanProduct.java | 1 +
.../global/LoanProductGlobalInitializerStep.java | 32 +++++++++
.../fineract/test/support/TestContextKey.java | 1 +
.../test/resources/features/LoanRepayment.feature | 83 ++++++++++++++++++++++
.../portfolio/loanaccount/domain/Loan.java | 19 +++++
.../service/LoanDownPaymentHandlerServiceImpl.java | 1 +
.../domain/LoanAccountDomainServiceJpa.java | 1 +
.../LoanTransactionProcessingServiceImpl.java | 3 +
.../integrationtests/BaseLoanIntegrationTest.java | 61 ++++++++++++----
.../LoanTransactionAccrualActivityPostingTest.java | 46 ++++++------
...nReprocessForAdvancedPaymentAllocationTest.java | 2 +-
...tiveLoansWithAdvancedPaymentAllocationTest.java | 18 ++---
12 files changed, 219 insertions(+), 49 deletions(-)
diff --git
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
index de24fdd195..5cd25a4e87 100644
---
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
+++
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java
@@ -109,6 +109,7 @@ public enum DefaultLoanProduct implements LoanProduct {
LP2_ADV_PYMNT_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR, //
LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_INTEREST_FIRST, //
LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_PRINCIPAL_FIRST, //
+ LP2_NO_INTEREST_RECALCULATION_ALLOCATION_PENALTY_FIRST, //
LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_EMI_360_30_CHARGEBACK_INTEREST_PENALTY_FEE_PRINCIPAL,
//
LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_EMI_360_30_CHARGEBACK_INTEREST_FEE_PRINCIPAL,
//
LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALC_EMI_360_30_CHARGEBACK_PRINCIPAL_INTEREST_FEE,
//
diff --git
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
index 6ef4a74c30..74e603d62c 100644
---
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
+++
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
@@ -3302,6 +3302,38 @@ public class LoanProductGlobalInitializerStep implements
FineractGlobalInitializ
TestContext.INSTANCE.set(
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_DOWNPAYMENT_INTEREST_FLAT_ADV_PMT_ALLOC_MULTIDISBURSE_PART_PERIOD_CALC_DISABLED,
responseLoanProductsRequestDownPaymentAdvInterestFlatMultiDisbPartPeriodIntCalcDisabled);
+
+ // LP2 without Down-payment + interest recalculation disabled +
advanced payment allocation + progressive loan
+ // schedule + horizontal + allocation penalty first
+ // (LP2_NO_INTEREST_RECALCULATION_ALLOCATION_PENALTY_FIRST)
+ String name127 =
DefaultLoanProduct.LP2_NO_INTEREST_RECALCULATION_ALLOCATION_PENALTY_FIRST.getName();
+ PostLoanProductsRequest
loanProductsRequestNoInterestRecalculationAllocationPenaltyFirst =
loanProductsRequestFactory
+ .defaultLoanProductsRequestLP2()//
+ .name(name127)//
+
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION.getValue())//
+ .loanScheduleType("PROGRESSIVE") //
+ .loanScheduleProcessingType("HORIZONTAL")//
+ .enableDownPayment(false)//
+ .enableAutoRepaymentForDownPayment(null)//
+ .disbursedAmountPercentageForDownPayment(null)//
+ .paymentAllocation(List.of(//
+ createPaymentAllocation("DEFAULT", "LAST_INSTALLMENT",
+
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_PENALTY, //
+
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_INTEREST, //
+
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_PRINCIPAL, //
+
LoanProductPaymentAllocationRule.AllocationTypesEnum.PAST_DUE_FEE, //
+
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_PENALTY, //
+
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_INTEREST, //
+
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_PRINCIPAL, //
+
LoanProductPaymentAllocationRule.AllocationTypesEnum.DUE_FEE, //
+
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_PENALTY, //
+
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_INTEREST, //
+
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_PRINCIPAL, //
+
LoanProductPaymentAllocationRule.AllocationTypesEnum.IN_ADVANCE_FEE)));//
+ Response<PostLoanProductsResponse>
responseLoanProductsRequestNoInterestRecalculationAllocationPenaltyFirst =
loanProductsApi
+
.createLoanProduct(loanProductsRequestNoInterestRecalculationAllocationPenaltyFirst).execute();
+
TestContext.INSTANCE.set(TestContextKey.LP2_NO_INTEREST_RECALCULATION_ALLOCATION_PENALTY_FIRST_RESPONSE,
+
responseLoanProductsRequestNoInterestRecalculationAllocationPenaltyFirst);
}
public static AdvancedPaymentData createPaymentAllocation(String
transactionType, String futureInstallmentAllocationRule,
diff --git
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
index 54fe29e5e7..9eefa43c57 100644
---
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
+++
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
@@ -169,6 +169,7 @@ public abstract class TestContextKey {
public static final String
DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_360_30_INTEREST_RECALC_AUTO_DOWNPAYMENT_ZERO_INTEREST_CHARGE_OFF_ACCRUAL_ACTIVITY
=
"loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationAutoDownpaymentZeroInterestChargeOffBehaviourAccrualActivity";
public static final String
LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_INTEREST_FIRST_RESPONSE =
"loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationInterestFirst";
public static final String
LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_PRINCIPAL_FIRST_RESPONSE =
"loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationPrincipalFirst";
+ public static final String
LP2_NO_INTEREST_RECALCULATION_ALLOCATION_PENALTY_FIRST_RESPONSE =
"loanProductCreateResponseLP2NoInterestRecalculationAllocationPenaltyFirst";
public static final String
DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR_LAST_INSTALLMENT_STRATEGY
=
"loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationAccelerateMaturityChargeOffBehaviourLastInstallmentStrategy";
public static final String
DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR_LAST_INSTALLMENT_STRATEGY
=
"loanProductCreateResponseLP2AdvancedPaymentAccelerateMaturityChargeOffBehaviourLastInstallmentStrategy";
public static final String
DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_IR_DAILY_TILL_REST_FREQUENCY_DATE_LAST_INSTALLMENT
=
"loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyTillRestFrequencyDateLastInstallment";
diff --git
a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature
b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature
index 7f32493ce1..d9ad3fe40d 100644
---
a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature
+++
b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature
@@ -5406,3 +5406,86 @@ Feature: LoanRepayment
| 01 January 2024 | Disbursement | 100.0 | 0.0 | 0.0 |
0.0 | 0.0 | 100.0 | false | false |
| 01 February 2024 | Repayment | 40.0 | 39.42 | 0.58 |
0.0 | 0.0 | 60.58 | false | false |
When Admin set
"LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL" loan product
"DEFAULT" transaction type to "NEXT_INSTALLMENT" future installment allocation
rule
+
+ Scenario: Verify progressive loan repayment reversals with penalty charge
and backdated repayment
+ When Admin sets the business date to "20 October 2024"
+ When Admin creates a client with random data
+ When Admin creates a fully customized loan with the following data:
+ | 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 |
+ | LP2_NO_INTEREST_RECALCULATION_ALLOCATION_PENALTY_FIRST | 20 October
2024 | 100 | 0 | FLAT |
SAME_AS_REPAYMENT_PERIOD | EQUAL_INSTALLMENTS | 30 | DAYS
| 30 | DAYS | 1 | 0
| 0 | 0 |
ADVANCED_PAYMENT_ALLOCATION |
+ And Admin successfully approves the loan on "20 October 2024" with "100"
amount and expected disbursement date on "20 October 2024"
+ And Admin successfully disburse the loan on "20 October 2024" with "100"
EUR transaction amount
+ Then Loan Repayment schedule has 1 periods, with the following data for
periods:
+ | Nr | Days | Date | Paid date | Balance of loan | Principal
due | Interest | Fees | Penalties | Due | Paid | In advance | Late |
Outstanding |
+ | | | 20 October 2024 | | 100.0 |
| | 0.0 | | 0.0 | 0.0 | | |
|
+ | 1 | 30 | 19 November 2024 | | 0.0 | 100.0
| 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0
|
+ Then Loan Repayment schedule has the following data in Total row:
+ | Principal due | Interest | Fees | Penalties | Due | Paid | In
advance | Late | Outstanding |
+ | 100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0
| 0.0 | 100.0 |
+ When Admin sets the business date to "22 October 2024"
+ And Customer makes "AUTOPAY" repayment on "22 October 2024" with 100 EUR
transaction amount
+ Then Loan status will be "CLOSED_OBLIGATIONS_MET"
+ Then Loan Repayment schedule has 1 periods, with the following data for
periods:
+ | Nr | Days | Date | Paid date | Balance of loan |
Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late
| Outstanding |
+ | | | 20 October 2024 | | 100.0 |
| | 0.0 | | 0.0 | 0.0 | | |
|
+ | 1 | 30 | 19 November 2024 | 22 October 2024 | 0.0 |
100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 100.0 | 100.0 | 0.0
| 0.0 |
+ Then Loan Transactions tab has the following data:
+ | Transaction date | Transaction Type | Amount | Principal | Interest |
Fees | Penalties | Loan Balance |
+ | 20 October 2024 | Disbursement | 100.0 | 0.0 | 0.0 |
0.0 | 0.0 | 100.0 |
+ | 22 October 2024 | Repayment | 100.0 | 100.0 | 0.0 |
0.0 | 0.0 | 0.0 |
+ When Admin sets the business date to "24 October 2024"
+ And Customer makes a repayment undo on "22 October 2024"
+ Then Loan status will be "ACTIVE"
+ And Loan has 100 outstanding amount
+ Then Loan Repayment schedule has 1 periods, with the following data for
periods:
+ | Nr | Days | Date | Paid date | Balance of loan | Principal
due | Interest | Fees | Penalties | Due | Paid | In advance | Late |
Outstanding |
+ | | | 20 October 2024 | | 100.0 |
| | 0.0 | | 0.0 | 0.0 | | |
|
+ | 1 | 30 | 19 November 2024 | | 0.0 | 100.0
| 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0
|
+ Then Loan Transactions tab has the following data:
+ | Transaction date | Transaction Type | Amount | Principal | Interest |
Fees | Penalties | Loan Balance | Reverted | Replayed |
+ | 20 October 2024 | Disbursement | 100.0 | 0.0 | 0.0 |
0.0 | 0.0 | 100.0 | false | false |
+ | 22 October 2024 | Repayment | 100.0 | 100.0 | 0.0 |
0.0 | 0.0 | 0.0 | true | false |
+ When Admin sets the business date to "26 October 2024"
+ And Customer makes "AUTOPAY" repayment on "26 October 2024" with 100 EUR
transaction amount
+ Then Loan status will be "CLOSED_OBLIGATIONS_MET"
+ Then Loan Repayment schedule has 1 periods, with the following data for
periods:
+ | Nr | Days | Date | Paid date | Balance of loan |
Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late
| Outstanding |
+ | | | 20 October 2024 | | 100.0 |
| | 0.0 | | 0.0 | 0.0 | | |
|
+ | 1 | 30 | 19 November 2024 | 26 October 2024 | 0.0 |
100.0 | 0.0 | 0.0 | 0.0 | 100.0 | 100.0 | 100.0 | 0.0
| 0.0 |
+ Then Loan Transactions tab has the following data:
+ | Transaction date | Transaction Type | Amount | Principal | Interest |
Fees | Penalties | Loan Balance | Reverted | Replayed |
+ | 20 October 2024 | Disbursement | 100.0 | 0.0 | 0.0 |
0.0 | 0.0 | 100.0 | false | false |
+ | 22 October 2024 | Repayment | 100.0 | 100.0 | 0.0 |
0.0 | 0.0 | 0.0 | true | false |
+ | 26 October 2024 | Repayment | 100.0 | 100.0 | 0.0 |
0.0 | 0.0 | 0.0 | false | false |
+ When Admin sets the business date to "28 October 2024"
+ And Customer makes a repayment undo on "26 October 2024"
+ Then Loan status will be "ACTIVE"
+ And Loan has 100 outstanding amount
+ Then Loan Repayment schedule has 1 periods, with the following data for
periods:
+ | Nr | Days | Date | Paid date | Balance of loan | Principal
due | Interest | Fees | Penalties | Due | Paid | In advance | Late |
Outstanding |
+ | | | 20 October 2024 | | 100.0 |
| | 0.0 | | 0.0 | 0.0 | | |
|
+ | 1 | 30 | 19 November 2024 | | 0.0 | 100.0
| 0.0 | 0.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | 100.0
|
+ When Admin adds "LOAN_NSF_FEE" due date charge with "28 October 2024" due
date and 10 EUR transaction amount
+ Then Loan Repayment schedule has 1 periods, with the following data for
periods:
+ | Nr | Days | Date | Paid date | Balance of loan | Principal
due | Interest | Fees | Penalties | Due | Paid | In advance | Late |
Outstanding |
+ | | | 20 October 2024 | | 100.0 |
| | 0.0 | | 0.0 | 0.0 | | |
|
+ | 1 | 30 | 19 November 2024 | | 0.0 | 100.0
| 0.0 | 0.0 | 10.0 | 110.0 | 0.0 | 0.0 | 0.0 | 110.0
|
+ Then Loan Repayment schedule has the following data in Total row:
+ | Principal due | Interest | Fees | Penalties | Due | Paid | In
advance | Late | Outstanding |
+ | 100.0 | 0.0 | 0.0 | 10.0 | 110.0 | 0.0 | 0.0
| 0.0 | 110.0 |
+ And Customer makes "AUTOPAY" repayment on "26 October 2024" with 101 EUR
transaction amount
+ Then Loan Repayment schedule has 1 periods, with the following data for
periods:
+ | Nr | Days | Date | Paid date | Balance of loan | Principal
due | Interest | Fees | Penalties | Due | Paid | In advance | Late |
Outstanding |
+ | | | 20 October 2024 | | 100.0 |
| | 0.0 | | 0.0 | 0.0 | | |
|
+ | 1 | 30 | 19 November 2024 | | 0.0 | 100.0
| 0.0 | 0.0 | 10.0 | 110.0 | 101.0 | 101.0 | 0.0 | 9.0
|
+ Then Loan Repayment schedule has the following data in Total row:
+ | Principal due | Interest | Fees | Penalties | Due | Paid | In
advance | Late | Outstanding |
+ | 100.0 | 0.0 | 0.0 | 10.0 | 110.0 | 101.0 | 101.0
| 0.0 | 9.0 |
+ Then Loan Transactions tab has the following data:
+ | Transaction date | Transaction Type | Amount | Principal | Interest |
Fees | Penalties | Loan Balance | Reverted | Replayed |
+ | 20 October 2024 | Disbursement | 100.0 | 0.0 | 0.0 |
0.0 | 0.0 | 100.0 | false | false |
+ | 22 October 2024 | Repayment | 100.0 | 100.0 | 0.0 |
0.0 | 0.0 | 0.0 | true | false |
+ | 26 October 2024 | Repayment | 100.0 | 100.0 | 0.0 |
0.0 | 0.0 | 0.0 | true | false |
+ | 26 October 2024 | Repayment | 101.0 | 100.0 | 0.0 |
0.0 | 1.0 | 0.0 | false | false |
+ When Customer makes "AUTOPAY" repayment on "27 October 2024" with 9 EUR
transaction amount
+ Then Loan status will be "CLOSED_OBLIGATIONS_MET"
\ No newline at end of file
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index 64458b8649..337b3fd472 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -49,11 +49,13 @@ import java.util.ListIterator;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
+import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import
org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom;
@@ -79,6 +81,7 @@ import
org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRef
import org.apache.fineract.portfolio.rate.domain.Rate;
import
org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
import org.apache.fineract.useradministration.domain.AppUser;
+import org.springframework.lang.NonNull;
@Entity
@Table(name = "m_loan", uniqueConstraints = { @UniqueConstraint(columnNames =
{ "account_no" }, name = "loan_account_no_UNIQUE"),
@@ -1242,6 +1245,22 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.charges == null ? new HashSet<>() :
this.charges.stream().filter(LoanCharge::isActive).collect(Collectors.toSet());
}
+ public boolean
hasChargesAffectedByBackdatedRepaymentLikeTransaction(@NonNull final
LoanTransaction transaction) {
+ if (!transaction.isRepaymentLikeType() ||
CollectionUtils.isEmpty(this.charges) || !isProgressiveSchedule()
+ ||
!DateUtils.isBeforeBusinessDate(transaction.getTransactionDate())) {
+ return false;
+ }
+
+ final BiFunction<LocalDate, LocalDate, LocalDate> earlierDate =
(date1, date2) -> DateUtils.isBefore(date1, date2) ? date1 : date2;
+
+ return this.charges.stream().filter(LoanCharge::isActive)
+ .filter(loanCharge -> loanCharge.isSpecifiedDueDate() ||
loanCharge.isOverdueInstallmentCharge())
+ .filter(loanCharge -> loanCharge.getDueLocalDate() !=
null).anyMatch(loanCharge -> {
+ final LocalDate comparisonDate =
earlierDate.apply(loanCharge.getDueLocalDate(),
loanCharge.getSubmittedOnDate());
+ return comparisonDate != null &&
comparisonDate.isAfter(transaction.getTransactionDate());
+ });
+ }
+
public LoanCharge fetchLoanChargesById(final Long id) {
LoanCharge charge = null;
for (LoanCharge loanCharge : this.charges) {
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java
index dfc76a4557..467b8dbaf6 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java
@@ -133,6 +133,7 @@ public class LoanDownPaymentHandlerServiceImpl implements
LoanDownPaymentHandler
boolean processLatest = isTransactionChronologicallyLatest //
&& adjustedTransaction == null // covers reversals
&& !loan.isForeclosure() //
+ &&
!loan.hasChargesAffectedByBackdatedRepaymentLikeTransaction(loanTransaction)
&&
loanTransactionProcessingService.canProcessLatestTransactionOnly(loan,
loanTransaction, currentInstallment); //
if (processLatest) {
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(),
loanTransaction,
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
index b36039fdb1..3ba2f187c1 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
@@ -904,6 +904,7 @@ public class LoanAccountDomainServiceJpa implements
LoanAccountDomainService {
boolean processLatest = isTransactionChronologicallyLatest //
&& !loan.isForeclosure() //
+ &&
!loan.hasChargesAffectedByBackdatedRepaymentLikeTransaction(refundTransaction)
//
&&
loanTransactionProcessingService.canProcessLatestTransactionOnly(loan,
refundTransaction, currentInstallment); //
if (processLatest) {
loanTransactionProcessingService.processLatestTransaction(loan.getTransactionProcessingStrategyCode(),
refundTransaction,
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java
index f5592c81be..e7aa89385b 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanTransactionProcessingServiceImpl.java
@@ -74,6 +74,9 @@ public class LoanTransactionProcessingServiceImpl implements
LoanTransactionProc
if
(!DateUtils.isEqualBusinessDate(loanTransaction.getTransactionDate())) {
return false;
}
+ if
(loan.hasChargesAffectedByBackdatedRepaymentLikeTransaction(loanTransaction)) {
+ return false;
+ }
LoanInterestRecalculationDetails interestRecalculationDetails =
loan.getLoanInterestRecalculationDetails();
if (interestRecalculationDetails != null &&
((interestRecalculationDetails.getRestFrequencyType().isSameAsRepayment()
&&
interestRecalculationDetails.getPreCloseInterestCalculationStrategy().calculateTillPreClosureDateEnabled())
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 6bb511131e..46ce4b44b0 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
@@ -645,26 +645,57 @@ public abstract class BaseLoanIntegrationTest extends
IntegrationTest {
}
}
- protected void verifyTransactions(Long loanId, TransactionExt...
transactions) {
- GetLoansLoanIdResponse loanDetails =
loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
+ protected void verifyTransactions(final Long loanId, final
TransactionExt... transactions) {
+ final GetLoansLoanIdResponse loanDetails =
loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
if (transactions == null || transactions.length == 0) {
assertNull(loanDetails.getTransactions(), "No transaction is
expected on loan " + loanId);
} else {
+ Assertions.assertNotNull(loanDetails.getTransactions());
Assertions.assertEquals(transactions.length,
loanDetails.getTransactions().size(), "Number of transactions on loan " +
loanId);
+
Arrays.stream(transactions).forEach(tr -> {
- boolean found = loanDetails.getTransactions().stream()
- .anyMatch(item ->
Objects.equals(Utils.getDoubleValue(item.getAmount()), tr.amount) //
- && Objects.equals(item.getType().getValue(),
tr.type) //
- && Objects.equals(item.getDate(),
LocalDate.parse(tr.date, dateTimeFormatter)) //
- &&
Objects.equals(Utils.getDoubleValue(item.getOutstandingLoanBalance()),
tr.outstandingPrincipal) //
- &&
Objects.equals(Utils.getDoubleValue(item.getPrincipalPortion()),
tr.principalPortion) //
- &&
Objects.equals(Utils.getDoubleValue(item.getInterestPortion()),
tr.interestPortion) //
- &&
Objects.equals(Utils.getDoubleValue(item.getFeeChargesPortion()),
tr.feePortion) //
- &&
Objects.equals(Utils.getDoubleValue(item.getPenaltyChargesPortion()),
tr.penaltyPortion) //
- &&
Objects.equals(Utils.getDoubleValue(item.getOverpaymentPortion()),
tr.overpaymentPortion) //
- &&
Objects.equals(Utils.getDoubleValue(item.getUnrecognizedIncomePortion()),
tr.unrecognizedPortion) //
- );
- Assertions.assertTrue(found, "Required transaction not found:
" + tr + " on loan " + loanId);
+ final List<GetLoansLoanIdTransactions> transactionsByDate =
loanDetails.getTransactions().stream()
+ .filter(item -> Objects.equals(item.getDate(),
LocalDate.parse(tr.date, dateTimeFormatter))).toList();
+
+ if (transactionsByDate.isEmpty()) {
+ Assertions.fail("No transactions found for date " +
tr.date + " on loan " + loanId);
+ return;
+ }
+
+ final boolean found = transactionsByDate.stream()
+ .anyMatch(item ->
Objects.equals(Utils.getDoubleValue(item.getAmount()), tr.amount)
+ && Objects.equals(item.getType().getValue(),
tr.type)
+ &&
Objects.equals(Utils.getDoubleValue(item.getOutstandingLoanBalance()),
tr.outstandingPrincipal)
+ &&
Objects.equals(Utils.getDoubleValue(item.getPrincipalPortion()),
tr.principalPortion)
+ &&
Objects.equals(Utils.getDoubleValue(item.getInterestPortion()),
tr.interestPortion)
+ &&
Objects.equals(Utils.getDoubleValue(item.getFeeChargesPortion()), tr.feePortion)
+ &&
Objects.equals(Utils.getDoubleValue(item.getPenaltyChargesPortion()),
tr.penaltyPortion)
+ &&
Objects.equals(Utils.getDoubleValue(item.getOverpaymentPortion()),
tr.overpaymentPortion)
+ &&
Objects.equals(Utils.getDoubleValue(item.getUnrecognizedIncomePortion()),
tr.unrecognizedPortion));
+
+ if (!found) {
+ final StringBuilder errorMessage = new StringBuilder();
+ errorMessage.append("Required transaction not found:
").append(tr).append(" on loan ").append(loanId);
+ errorMessage.append("\nTransactions found for date
").append(tr.date).append(":");
+
+ for (int i = 0; i < transactionsByDate.size(); i++) {
+ GetLoansLoanIdTransactions item =
transactionsByDate.get(i);
+ errorMessage.append("\n Transaction ").append(i +
1).append(": ");
+
errorMessage.append("amount=").append(Utils.getDoubleValue(item.getAmount()));
+ errorMessage.append(",
type=").append(item.getType().getValue());
+ errorMessage.append(",
date=").append(item.getDate().format(dateTimeFormatter));
+ errorMessage.append(",
outstandingPrincipal=").append(Utils.getDoubleValue(item.getOutstandingLoanBalance()));
+ errorMessage.append(",
principalPortion=").append(Utils.getDoubleValue(item.getPrincipalPortion()));
+ errorMessage.append(",
interestPortion=").append(Utils.getDoubleValue(item.getInterestPortion()));
+ errorMessage.append(",
feePortion=").append(Utils.getDoubleValue(item.getFeeChargesPortion()));
+ errorMessage.append(",
penaltyPortion=").append(Utils.getDoubleValue(item.getPenaltyChargesPortion()));
+ errorMessage.append(",
unrecognizedPortion=").append(Utils.getDoubleValue(item.getUnrecognizedIncomePortion()));
+ errorMessage.append(",
overpaymentPortion=").append(Utils.getDoubleValue(item.getOverpaymentPortion()));
+ errorMessage.append(",
reversed=").append(item.getManuallyReversed() != null ?
item.getManuallyReversed() : false);
+ }
+
+ Assertions.fail(errorMessage.toString());
+ }
});
}
}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java
index 7b0a7a584c..5de5032340 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionAccrualActivityPostingTest.java
@@ -714,7 +714,6 @@ public class LoanTransactionAccrualActivityPostingTest
extends BaseLoanIntegrati
final String creationBusinessDay = "15 January 2023";
AtomicReference<Long> loanId = new AtomicReference<>();
runAt(creationBusinessDay, () -> {
-
Long localLoanProductId =
createLoanProductAccountingAccrualPeriodicAdvancedPaymentAllocation();
loanId.set(applyForLoanApplicationAdvancedPaymentAllocation(client.getClientId(),
localLoanProductId, BigDecimal.valueOf(40000),
disbursementDay, BigDecimal.ZERO));
@@ -723,53 +722,52 @@ public class LoanTransactionAccrualActivityPostingTest
extends BaseLoanIntegrati
.dateFormat(DATETIME_PATTERN).approvedOnDate(disbursementDay).locale("en"));
loanTransactionHelper.disburseLoan(loanId.get(), new
PostLoansLoanIdRequest().actualDisbursementDate(disbursementDay)
-
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
+
.dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(1000)).locale("en"));
chargePenalty(loanId.get(), 20.0, chargeDueDate1st);
addRepaymentForLoan(loanId.get(), 50.0, "10 January 2023");
verifyTransactions(loanId.get(), //
- transaction(1000.0, "Disbursement", disbursementDay,
1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
- transaction(50.0, "Repayment", "10 January 2023", 970, 30,
0, 0, 20, 0.0, 0.0));
+ transaction(1000, "Disbursement", disbursementDay, 1000,
0, 0, 0, 0, 0, 0),
+ transaction(50, "Repayment", "10 January 2023", 950, 50,
0, 0, 0, 0, 0));
});
runAt(repaymentPeriod1CloseDate, () -> {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId.get()));
verifyTransactions(loanId.get(), //
- transaction(1000.0, "Disbursement", disbursementDay,
1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
- transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0,
0, 20, 0.0, 0.0),
- transaction(50.0, "Repayment", "10 January 2023", 970, 30,
0, 0, 20, 0.0, 0.0),
- transaction(20.0, "Accrual Activity", "01 February 2023",
0, 0, 0.0, 0.0, 20.0, 0.0, 0.0));
+ transaction(1000, "Disbursement", disbursementDay, 1000,
0, 0, 0, 0, 0, 0),
+ transaction(20, "Accrual", "01 February 2023", 0, 0, 0, 0,
20, 0, 0),
+ transaction(50, "Repayment", "10 January 2023", 950, 50,
0, 0, 0, 0, 0),
+ transaction(20, "Accrual Activity", "01 February 2023", 0,
0, 0, 0, 20, 0, 0));
});
runAt(repaymentPeriod1OneDayAfterCloseDate, () -> {
-
addRepaymentForLoan(loanId.get(), 220.0, "8 January 2023");
verifyTransactions(loanId.get(), //
- transaction(1000.0, "Disbursement", disbursementDay,
1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
- transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0,
0, 20, 0.0, 0.0),
- transaction(50.0, "Repayment", "10 January 2023", 730, 50,
0, 0, 0, 0.0, 0.0),
- transaction(220.0, "Repayment", "08 January 2023", 780,
220, 0, 0, 0, 0.0, 0.0),
- transaction(20.0, "Accrual Activity", "01 February 2023",
0, 0, 0.0, 0.0, 20.0, 0.0, 0.0));
+ transaction(1000, "Disbursement", disbursementDay, 1000,
0, 0, 0, 0, 0, 0),
+ transaction(20, "Accrual", "01 February 2023", 0, 0, 0, 0,
20, 0, 0),
+ transaction(50, "Repayment", "10 January 2023", 730, 50,
0, 0, 0, 0, 0),
+ transaction(220, "Repayment", "08 January 2023", 780, 220,
0, 0, 0, 0, 0),
+ transaction(20, "Accrual Activity", "01 February 2023", 0,
0, 0, 0, 20, 0, 0));
chargePenalty(loanId.get(), 33.0, chargeDueDate2st);
verifyTransactions(loanId.get(), //
- transaction(1000.0, "Disbursement", disbursementDay,
1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
- transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0,
0, 20, 0.0, 0.0),
- transaction(50.0, "Repayment", "10 January 2023", 730, 50,
0, 0, 0, 0.0, 0.0),
- transaction(220.0, "Repayment", "08 January 2023", 780,
220, 0, 0, 0, 0.0, 0.0),
- transaction(53.0, "Accrual Activity", "01 February 2023",
0, 0, 0.0, 0.0, 53.0, 0.0, 0.0));
+ transaction(1000, "Disbursement", disbursementDay, 1000,
0, 0, 0, 0, 0, 0),
+ transaction(20, "Accrual", "01 February 2023", 0, 0, 0, 0,
20, 0, 0),
+ transaction(50, "Repayment", "10 January 2023", 730, 50,
0, 0, 0, 0, 0),
+ transaction(220, "Repayment", "08 January 2023", 780, 220,
0, 0, 0, 0, 0),
+ transaction(53, "Accrual Activity", "01 February 2023", 0,
0, 0, 0, 53, 0, 0));
chargeFee(loanId.get(), 12.0, chargeDueDate3st);
verifyTransactions(loanId.get(), //
- transaction(1000.0, "Disbursement", disbursementDay,
1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
- transaction(20.0, "Accrual", "01 February 2023", 0, 0, 0,
0, 20, 0.0, 0.0),
- transaction(50.0, "Repayment", "10 January 2023", 730, 50,
0, 0, 0, 0.0, 0.0),
- transaction(220.0, "Repayment", "08 January 2023", 780,
220, 0, 0, 0, 0.0, 0.0),
- transaction(65.0, "Accrual Activity", "01 February 2023",
0, 0, 0.0, 12.0, 53.0, 0.0, 0.0));
+ transaction(1000, "Disbursement", disbursementDay, 1000,
0, 0, 0, 0, 0, 0),
+ transaction(20, "Accrual", "01 February 2023", 0, 0, 0, 0,
20, 0, 0),
+ transaction(50, "Repayment", "10 January 2023", 730, 50,
0, 0, 0, 0, 0),
+ transaction(220, "Repayment", "08 January 2023", 780, 220,
0, 0, 0, 0, 0),
+ transaction(65, "Accrual Activity", "01 February 2023", 0,
0, 0, 12, 53, 0, 0));
});
}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionReprocessForAdvancedPaymentAllocationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionReprocessForAdvancedPaymentAllocationTest.java
index 92774f3b53..158e7a2639 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionReprocessForAdvancedPaymentAllocationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionReprocessForAdvancedPaymentAllocationTest.java
@@ -120,7 +120,7 @@ public class
LoanTransactionReprocessForAdvancedPaymentAllocationTest extends Ba
.transactionAmount(50.0).externalId(loanTransactionExternalIdStr));
// verify transaction amounts
- verifyTransaction(LocalDate.of(2023, 2, 20), 50.0f, 0.0f, 0.0f,
50.0f, 0.0f, loanId, "repayment");
+ verifyTransaction(LocalDate.of(2023, 2, 20), 50.0f, 50.0f, 0.0f,
0.0f, 0.0f, loanId, "repayment");
// add loan charge for a date later than repayment date
// apply penalty
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java
index 35a2b1daac..6499611820 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java
@@ -389,11 +389,11 @@ public class
RefundForActiveLoansWithAdvancedPaymentAllocationTest extends BaseL
assertEquals(LocalDate.of(2023, 1, 31),
firstRepaymentInstallment.getDueDate());
assertEquals(feePortion,
Utils.getDoubleValue(secondRepaymentInstallment.getFeeChargesDue()));
- assertEquals(0.00,
Utils.getDoubleValue(secondRepaymentInstallment.getFeeChargesOutstanding()));
+ assertEquals(50.00,
Utils.getDoubleValue(secondRepaymentInstallment.getFeeChargesOutstanding()));
assertEquals(penaltyPortion,
Utils.getDoubleValue(secondRepaymentInstallment.getPenaltyChargesDue()));
- assertEquals(0.00,
Utils.getDoubleValue(secondRepaymentInstallment.getPenaltyChargesOutstanding()));
+ assertEquals(100.00,
Utils.getDoubleValue(secondRepaymentInstallment.getPenaltyChargesOutstanding()));
assertEquals(400.00,
Utils.getDoubleValue(secondRepaymentInstallment.getTotalDueForPeriod()));
- assertEquals(0.00,
Utils.getDoubleValue(secondRepaymentInstallment.getTotalOutstandingForPeriod()));
+ assertEquals(150.00,
Utils.getDoubleValue(secondRepaymentInstallment.getTotalOutstandingForPeriod()));
assertEquals(LocalDate.of(2023, 3, 2),
secondRepaymentInstallment.getDueDate());
assertEquals(0.00,
Utils.getDoubleValue(thirdRepaymentInstallment.getFeeChargesDue()));
@@ -401,7 +401,7 @@ public class
RefundForActiveLoansWithAdvancedPaymentAllocationTest extends BaseL
assertEquals(0.00,
Utils.getDoubleValue(thirdRepaymentInstallment.getPenaltyChargesDue()));
assertEquals(0.00,
Utils.getDoubleValue(thirdRepaymentInstallment.getPenaltyChargesOutstanding()));
assertEquals(250.00,
Utils.getDoubleValue(thirdRepaymentInstallment.getTotalDueForPeriod()));
- assertEquals(240.00,
Utils.getDoubleValue(thirdRepaymentInstallment.getTotalOutstandingForPeriod()));
+ assertEquals(90.00,
Utils.getDoubleValue(thirdRepaymentInstallment.getTotalOutstandingForPeriod()));
assertEquals(LocalDate.of(2023, 4, 1),
thirdRepaymentInstallment.getDueDate());
loanTransactionHelper.makeRefundByCash("28 January 2023", 15.0f,
loanId);
@@ -424,13 +424,13 @@ public class
RefundForActiveLoansWithAdvancedPaymentAllocationTest extends BaseL
assertEquals(LocalDate.of(2023, 1, 31),
firstRepaymentInstallment.getDueDate());
assertEquals(feePortion,
Utils.getDoubleValue(secondRepaymentInstallment.getFeeChargesDue()));
- assertEquals(0.00,
Utils.getDoubleValue(secondRepaymentInstallment.getFeeChargesOutstanding()));
+ assertEquals(50.00,
Utils.getDoubleValue(secondRepaymentInstallment.getFeeChargesOutstanding()));
assertEquals(penaltyPortion,
Utils.getDoubleValue(secondRepaymentInstallment.getPenaltyChargesDue()));
- assertEquals(0.00,
Utils.getDoubleValue(secondRepaymentInstallment.getPenaltyChargesOutstanding()));
+ assertEquals(100.00,
Utils.getDoubleValue(secondRepaymentInstallment.getPenaltyChargesOutstanding()));
assertEquals(250.00,
Utils.getDoubleValue(secondRepaymentInstallment.getPrincipalDue()));
- assertEquals(5.00,
Utils.getDoubleValue(secondRepaymentInstallment.getPrincipalOutstanding()));
+ assertEquals(0.00,
Utils.getDoubleValue(secondRepaymentInstallment.getPrincipalOutstanding()));
assertEquals(400.00,
Utils.getDoubleValue(secondRepaymentInstallment.getTotalDueForPeriod()));
- assertEquals(5.00,
Utils.getDoubleValue(secondRepaymentInstallment.getTotalOutstandingForPeriod()));
+ assertEquals(150.00,
Utils.getDoubleValue(secondRepaymentInstallment.getTotalOutstandingForPeriod()));
assertEquals(LocalDate.of(2023, 3, 2),
secondRepaymentInstallment.getDueDate());
assertEquals(0.00,
Utils.getDoubleValue(thirdRepaymentInstallment.getFeeChargesDue()));
@@ -438,7 +438,7 @@ public class
RefundForActiveLoansWithAdvancedPaymentAllocationTest extends BaseL
assertEquals(0.00,
Utils.getDoubleValue(thirdRepaymentInstallment.getPenaltyChargesDue()));
assertEquals(0.00,
Utils.getDoubleValue(thirdRepaymentInstallment.getPenaltyChargesOutstanding()));
assertEquals(250.00,
Utils.getDoubleValue(thirdRepaymentInstallment.getTotalDueForPeriod()));
- assertEquals(250.00,
Utils.getDoubleValue(thirdRepaymentInstallment.getTotalOutstandingForPeriod()));
+ assertEquals(105.0,
Utils.getDoubleValue(thirdRepaymentInstallment.getTotalOutstandingForPeriod()));
assertEquals(LocalDate.of(2023, 4, 1),
thirdRepaymentInstallment.getDueDate());
// fully unpaying the second installment