From fc93db16b59ebb84a1844266a94b99f61bf87088 Mon Sep 17 00:00:00 2001 From: Oleksii Novikov Date: Mon, 30 Jun 2025 14:13:43 +0300 Subject: [PATCH] FINERACT-2319: Add penalty-first payment allocation strategy for progressive loans with backdated repayment charge handling --- .../data/loanproduct/DefaultLoanProduct.java | 1 + .../LoanProductGlobalInitializerStep.java | 32 +++++++ .../fineract/test/support/TestContextKey.java | 1 + .../resources/features/LoanRepayment.feature | 83 +++++++++++++++++++ .../portfolio/loanaccount/domain/Loan.java | 19 +++++ .../LoanDownPaymentHandlerServiceImpl.java | 1 + .../domain/LoanAccountDomainServiceJpa.java | 1 + .../LoanTransactionProcessingServiceImpl.java | 3 + .../BaseLoanIntegrationTest.java | 61 ++++++++++---- ...TransactionAccrualActivityPostingTest.java | 46 +++++----- ...ocessForAdvancedPaymentAllocationTest.java | 2 +- ...oansWithAdvancedPaymentAllocationTest.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 de24fdd19..5cd25a4e8 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 6ef4a74c3..74e603d62 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 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 54fe29e5e..9eefa43c5 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 7f32493ce..d9ad3fe40 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 64458b864..337b3fd47 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 { 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 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 dfc76a455..467b8dbaf 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 b36039fdb..3ba2f187c 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 f5592c81b..e7aa89385 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 6bb511131..46ce4b44b 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 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 7b0a7a584..5de503234 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 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 92774f3b5..158e7a263 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 35a2b1daa..649961182 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