From d56cf85dcdfc32c0568d325577344b71aaf7b354 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Fri, 19 Jan 2024 10:54:08 +0100 Subject: [PATCH] FINERACT-1968: Fix overpayment calculation --- .../portfolio/loanaccount/domain/Loan.java | 13 +-- .../loanaccount/domain/LoanTransaction.java | 5 -- ...RepaymentScheduleTransactionProcessor.java | 82 ++++++------------- ...RepaymentScheduleTransactionProcessor.java | 2 +- .../transactionprocessor/MoneyHolder.java | 32 ++++++++ ...edPaymentScheduleTransactionProcessor.java | 42 ++++++---- .../LoanChargeWritePlatformServiceImpl.java | 3 +- ...ymentScheduleTransactionProcessorTest.java | 10 ++- ...nPaymentTypeRepaymentTransactionsTest.java | 4 - 9 files changed, 100 insertions(+), 93 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/MoneyHolder.java 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 dbd14e80a5b..0235c8b6bdd 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 @@ -111,6 +111,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; @@ -797,7 +798,7 @@ private void handleChargePaidTransaction(final LoanCharge charge, final LoanTran final Set loanCharges = new HashSet<>(1); loanCharges.add(charge); loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargesPayment, getCurrency(), chargePaymentInstallments, - loanCharges, getTotalOverpaidAsMoney()); + loanCharges, new MoneyHolder(getTotalOverpaidAsMoney())); updateLoanSummaryDerivedFields(); doPostLoanTransactionChecks(chargesPayment.getTransactionDate(), loanLifecycleStateMachine); @@ -3321,7 +3322,7 @@ private ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(fi if (isTransactionChronologicallyLatest && adjustedTransaction == null && (!reprocess || !this.repaymentScheduleDetail().isInterestRecalculationEnabled()) && !isForeclosure()) { loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, getCurrency(), - getRepaymentScheduleInstallments(), getActiveCharges(), getTotalOverpaidAsMoney()); + getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())); reprocess = false; if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) { @@ -3914,7 +3915,7 @@ public ChangedTransactionDetail closeAsWrittenOff(final JsonCommand command, fin addLoanTransaction(loanTransaction); loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, loanCurrency(), - getRepaymentScheduleInstallments(), getActiveCharges(), getTotalOverpaidAsMoney()); + getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())); updateLoanSummaryDerivedFields(); } @@ -4019,7 +4020,7 @@ public ChangedTransactionDetail close(final JsonCommand command, final LoanLifec addLoanTransaction(loanTransaction); loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, loanCurrency(), - getRepaymentScheduleInstallments(), getActiveCharges(), getTotalOverpaidAsMoney()); + getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())); updateLoanSummaryDerivedFields(); } else if (totalOutstanding.isGreaterThanZero()) { @@ -6373,7 +6374,7 @@ private ChangedTransactionDetail handleRefundTransaction(final LoanTransaction l // If is a refund if (adjustedTransaction == null) { loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, getCurrency(), - getRepaymentScheduleInstallments(), getActiveCharges(), getTotalOverpaidAsMoney()); + getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())); } else { final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement(); changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), @@ -6404,7 +6405,7 @@ public void handleChargebackTransaction(final LoanTransaction chargebackTransact addLoanTransaction(chargebackTransaction); loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargebackTransaction, getCurrency(), - getRepaymentScheduleInstallments(), getActiveCharges(), getTotalOverpaidAsMoney()); + getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney())); updateLoanSummaryDerivedFields(); if (!doPostLoanTransactionChecks(chargebackTransaction.getTransactionDate(), loanLifecycleStateMachine)) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index bca0056329d..7226e6f23e0 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -483,11 +483,6 @@ public void updateComponentsAndTotal(final Money principal, final Money interest .plus(getPenaltyChargesPortion(currency)).getAmount(); } - public void updateOverPayments(final Money overPayment) { - final MonetaryCurrency currency = overPayment.getCurrency(); - this.overPaymentPortion = defaultToNullIfZero(getOverPaymentPortion(currency).plus(overPayment).getAmount()); - } - public void setOverPayments(final Money overPayment) { if (overPayment != null) { this.overPaymentPortion = defaultToNullIfZero(overPayment.getAmount()); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 8ca79d24319..89a2ec7b470 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -144,7 +144,7 @@ public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur if (unprocessed.isGreaterThanZero()) { onLoanOverpayment(loanTransaction, unprocessed); - loanTransaction.updateOverPayments(unprocessed); + loanTransaction.setOverPayments(unprocessed); } } else { @@ -152,6 +152,7 @@ public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur } } + MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(currency)); for (final LoanTransaction loanTransaction : transactionsToBeProcessed) { // TODO: analyze and remove this if (!loanTransaction.getTypeOf().equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN)) { @@ -163,7 +164,7 @@ public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { // pass through for new transactions if (loanTransaction.getId() == null) { - processLatestTransaction(loanTransaction, currency, installments, charges, null); + processLatestTransaction(loanTransaction, currency, installments, charges, overpaymentHolder); loanTransaction.adjustInterestComponent(currency); } else { /** @@ -174,7 +175,7 @@ public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur // Reset derived component of new loan transaction and // re-process transaction - processLatestTransaction(newLoanTransaction, currency, installments, charges, null); + processLatestTransaction(newLoanTransaction, currency, installments, charges, overpaymentHolder); newLoanTransaction.adjustInterestComponent(currency); /** * Check if the transaction amounts have changed. If so, reverse the original transaction and update @@ -195,9 +196,11 @@ public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur loanTransaction.resetDerivedComponents(); handleRefund(loanTransaction, currency, installments, charges); } else if (loanTransaction.isCreditBalanceRefund()) { - recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed); + recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed, + overpaymentHolder); } else if (loanTransaction.isChargeback()) { - recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed); + recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed, + overpaymentHolder); reprocessChargebackTransactionRelation(changedTransactionDetail, transactionsToBeProcessed); } else if (loanTransaction.isChargeOff()) { recalculateChargeOffTransaction(changedTransactionDetail, loanTransaction, currency, installments); @@ -209,11 +212,11 @@ public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbur @Override public void processLatestTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency, - final List installments, final Set charges, Money overpaidAmount) { + final List installments, final Set charges, MoneyHolder overpaymentHolder) { switch (loanTransaction.getTypeOf()) { case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments); case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges); - case CHARGEBACK -> handleChargeback(loanTransaction, currency, overpaidAmount, installments); + case CHARGEBACK -> handleChargeback(loanTransaction, currency, installments, overpaymentHolder); default -> { Money transactionAmountUnprocessed = handleTransactionAndCharges(loanTransaction, currency, installments, charges, null, false); @@ -223,8 +226,11 @@ public void processLatestTransaction(final LoanTransaction loanTransaction, fina transactionAmountUnprocessed.zero(), transactionAmountUnprocessed.zero()); } else { onLoanOverpayment(loanTransaction, transactionAmountUnprocessed); - loanTransaction.updateOverPayments(transactionAmountUnprocessed); + loanTransaction.setOverPayments(transactionAmountUnprocessed); } + overpaymentHolder.setMoneyObject(transactionAmountUnprocessed); + } else { + overpaymentHolder.setMoneyObject(Money.zero(currency)); } } } @@ -435,17 +441,15 @@ private boolean isNotObligationsMet(LoanRepaymentScheduleInstallment loanRepayme } private void recalculateCreditTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction loanTransaction, - MonetaryCurrency currency, List installments, - List transactionsToBeProcessed) { + MonetaryCurrency currency, List installments, List transactionsToBeProcessed, + MoneyHolder overpaymentHolder) { // pass through for new transactions if (loanTransaction.getId() == null) { return; } final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); - List mergedList = getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail); - Money overpaidAmount = calculateOverpaidAmount(loanTransaction, mergedList, installments, currency); - processCreditTransaction(newLoanTransaction, overpaidAmount, currency, installments); + processCreditTransaction(newLoanTransaction, overpaymentHolder, currency, installments); if (!LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) { createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail); } @@ -470,40 +474,7 @@ protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransac } - private Money calculateOverpaidAmount(LoanTransaction loanTransaction, List transactions, - List installments, MonetaryCurrency currency) { - Money totalPaidInRepayments = Money.zero(currency); - - Money cumulativeTotalPaidOnInstallments = Money.zero(currency); - for (final LoanRepaymentScheduleInstallment scheduledRepayment : installments) { - cumulativeTotalPaidOnInstallments = cumulativeTotalPaidOnInstallments - .plus(scheduledRepayment.getPrincipalCompleted(currency).plus(scheduledRepayment.getInterestPaid(currency))) - .plus(scheduledRepayment.getFeeChargesPaid(currency)).plus(scheduledRepayment.getPenaltyChargesPaid(currency)); - } - - for (final LoanTransaction transaction : transactions) { - if (transaction.isReversed()) { - continue; - } - if (transaction.equals(loanTransaction)) { - // We want to process only the transactions prior to the actual one - break; - } - if (transaction.isRefund() || transaction.isRefundForActiveLoan()) { - totalPaidInRepayments = totalPaidInRepayments.minus(transaction.getAmount(currency)); - } else if (transaction.isCreditBalanceRefund() || transaction.isChargeback()) { - totalPaidInRepayments = totalPaidInRepayments.minus(transaction.getOverPaymentPortion(currency)); - } else if (transaction.isRepaymentLikeType()) { - totalPaidInRepayments = totalPaidInRepayments.plus(transaction.getAmount(currency)); - } - } - - // if total paid in transactions higher than repayment schedule then - // theres an overpayment. - return MathUtil.negativeToZero(totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments)); - } - - private void processCreditTransaction(LoanTransaction loanTransaction, Money overpaidAmount, MonetaryCurrency currency, + private void processCreditTransaction(LoanTransaction loanTransaction, MoneyHolder overpaymentHolder, MonetaryCurrency currency, List installments) { loanTransaction.resetDerivedComponents(); List transactionMappings = new ArrayList<>(); @@ -511,9 +482,10 @@ private void processCreditTransaction(LoanTransaction loanTransaction, Money ove installments.sort(byDate); final Money zeroMoney = Money.zero(currency); Money transactionAmount = loanTransaction.getAmount(currency); - Money principalPortion = MathUtil.negativeToZero(loanTransaction.getAmount(currency).minus(overpaidAmount)); + Money principalPortion = MathUtil.negativeToZero(loanTransaction.getAmount(currency).minus(overpaymentHolder.getMoneyObject())); Money repaidAmount = MathUtil.negativeToZero(transactionAmount.minus(principalPortion)); - loanTransaction.updateOverPayments(repaidAmount); + loanTransaction.setOverPayments(repaidAmount); + overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().minus(repaidAmount)); loanTransaction.updateComponents(principalPortion, zeroMoney, zeroMoney, zeroMoney); if (principalPortion.isGreaterThanZero()) { @@ -770,14 +742,14 @@ protected void handleWriteOff(final LoanTransaction loanTransaction, final Monet loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion); } - protected void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount, - List installments) { - processCreditTransaction(loanTransaction, overpaidAmount, currency, installments); + protected void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, + List installments, MoneyHolder overpaidAmountHolder) { + processCreditTransaction(loanTransaction, overpaidAmountHolder, currency, installments); } - protected void handleCreditBalanceRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount, - List installments) { - processCreditTransaction(loanTransaction, overpaidAmount, currency, installments); + protected void handleCreditBalanceRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, + List installments, MoneyHolder overpaidAmountHolder) { + processCreditTransaction(loanTransaction, overpaidAmountHolder, currency, installments); } protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java index efcfba19ba4..ebf16d6a714 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java @@ -41,7 +41,7 @@ public interface LoanRepaymentScheduleTransactionProcessor { * schedule. */ void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Set charges, Money overpaidAmount); + List installments, Set charges, MoneyHolder overpaymentHolder); /** * Provides support for passing all {@link LoanTransaction}'s so it will completely re-process the entire loan diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/MoneyHolder.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/MoneyHolder.java new file mode 100644 index 00000000000..f967e8ffb4a --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/MoneyHolder.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.apache.fineract.organisation.monetary.domain.Money; + +@Getter +@Setter +@AllArgsConstructor +public class MoneyHolder { + + private Money moneyObject; +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 295b354d666..c63291b5a5f 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -55,6 +55,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanproduct.domain.DueType; import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; @@ -136,9 +137,10 @@ public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursement List chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges); final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); + MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(currency)); for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) { chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, currency, - installments, charges, changedTransactionDetail)); + installments, charges, changedTransactionDetail, overpaymentHolder)); chargeOrTransaction.getLoanCharge() .ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate)); } @@ -150,18 +152,18 @@ public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursement @Override public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Set charges, Money overpaidAmount) { + List installments, Set charges, MoneyHolder overpaymentHolder) { switch (loanTransaction.getTypeOf()) { case DISBURSEMENT -> handleDisbursement(loanTransaction, currency, installments); case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments); case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges); - case CHARGEBACK -> handleChargeback(loanTransaction, currency, overpaidAmount, installments); - case CREDIT_BALANCE_REFUND -> handleCreditBalanceRefund(loanTransaction, currency, overpaidAmount, installments); + case CHARGEBACK -> handleChargeback(loanTransaction, currency, installments, overpaymentHolder); + case CREDIT_BALANCE_REFUND -> handleCreditBalanceRefund(loanTransaction, currency, installments, overpaymentHolder); case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT, WAIVE_INTEREST, RECOVERY_REPAYMENT -> - handleRepayment(loanTransaction, currency, installments, charges); + handleRepayment(loanTransaction, currency, installments, charges, overpaymentHolder); case CHARGE_OFF -> handleChargeOff(loanTransaction, currency, installments); - case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, currency, installments, charges); + case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, currency, installments, charges, overpaymentHolder); case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed."); // TODO: Cover rest of the transaction types default -> { @@ -226,10 +228,10 @@ protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency cu } private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Set charges, - ChangedTransactionDetail changedTransactionDetail) { + List installments, Set charges, ChangedTransactionDetail changedTransactionDetail, + MoneyHolder overpaymentHolder) { if (loanTransaction.getId() == null) { - processLatestTransaction(loanTransaction, currency, installments, charges, Money.zero(currency)); + processLatestTransaction(loanTransaction, currency, installments, charges, overpaymentHolder); if (loanTransaction.isInterestWaiver()) { loanTransaction.adjustInterestComponent(currency); } @@ -242,7 +244,7 @@ private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryC // Reset derived component of new loan transaction and // re-process transaction - processLatestTransaction(newLoanTransaction, currency, installments, charges, Money.zero(currency)); + processLatestTransaction(newLoanTransaction, currency, installments, charges, overpaymentHolder); if (loanTransaction.isInterestWaiver()) { newLoanTransaction.adjustInterestComponent(currency); } @@ -322,12 +324,12 @@ private void updateLoanSchedule(LoanTransaction disbursementTransaction, Monetar } private void handleRepayment(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Set charges) { + List installments, Set charges, MoneyHolder overpaymentHolder) { if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { loanTransaction.resetDerivedComponents(); } Money transactionAmountUnprocessed = loanTransaction.getAmount(currency); - processTransaction(loanTransaction, currency, installments, transactionAmountUnprocessed, charges); + processTransaction(loanTransaction, currency, installments, transactionAmountUnprocessed, charges, overpaymentHolder); } private LoanTransactionToRepaymentScheduleMapping getTransactionMapping( @@ -399,10 +401,13 @@ private void addToTransactionMapping(LoanTransactionToRepaymentScheduleMapping l loanTransactionToRepaymentScheduleMapping.setComponents(aggregatedPrincipal, aggregatedInterest, aggregatedFee, aggregatedPenalty); } - private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction) { + private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction, MoneyHolder overpaymentHolder) { if (overpaymentPortion.isGreaterThanZero()) { onLoanOverpayment(loanTransaction, overpaymentPortion); - loanTransaction.updateOverPayments(overpaymentPortion); + overpaymentHolder.setMoneyObject(overpaymentPortion); + loanTransaction.setOverPayments(overpaymentPortion); + } else { + overpaymentHolder.setMoneyObject(overpaymentPortion.zero()); } } @@ -427,7 +432,7 @@ private void handleChargeOff(LoanTransaction loanTransaction, MonetaryCurrency c } private void handleChargePayment(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Set charges) { + List installments, Set charges, MoneyHolder overpaymentHolder) { Money zero = Money.zero(currency); Money feeChargesPortion = zero; Money penaltyChargesPortion = zero; @@ -473,7 +478,7 @@ private void handleChargePayment(LoanTransaction loanTransaction, MonetaryCurren } if (unprocessed.isGreaterThanZero()) { - processTransaction(loanTransaction, currency, installments, unprocessed, charges); + processTransaction(loanTransaction, currency, installments, unprocessed, charges, overpaymentHolder); } } @@ -658,7 +663,8 @@ private static List getFutureInstallmentsForRe } private void processTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, - List installments, Money transactionAmountUnprocessed, Set charges) { + List installments, Money transactionAmountUnprocessed, Set charges, + MoneyHolder overpaymentHolder) { Money zero = Money.zero(currency); List transactionMappings = new ArrayList<>(); @@ -683,7 +689,7 @@ private void processTransaction(LoanTransaction loanTransaction, MonetaryCurrenc balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion()); loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); - handleOverpayment(transactionAmountUnprocessed, loanTransaction); + handleOverpayment(transactionAmountUnprocessed, loanTransaction, overpaymentHolder); } private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index 5e96c43d996..d6391836f2d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -114,6 +114,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; @@ -832,7 +833,7 @@ private LoanTransaction applyChargeAdjustment(final Loan loan, final LoanCharge final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory .determineProcessor(loan.transactionProcessingStrategy()); loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanChargeAdjustmentTransaction, loan.getCurrency(), - loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), loan.getTotalOverpaidAsMoney()); + loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney())); loan.addLoanTransaction(loanChargeAdjustmentTransaction); loan.updateLoanSummaryAndStatus(); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java index be3f43f5bc1..e2ae5fa4059 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java @@ -43,6 +43,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; @@ -121,7 +122,8 @@ public void chargePaymentTransactionTestWithExactAmount() { Mockito.when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney); Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false); - underTest.processLatestTransaction(loanTransaction, currency, List.of(installment), Set.of(charge), overpaidAmount); + underTest.processLatestTransaction(loanTransaction, currency, List.of(installment), Set.of(charge), + new MoneyHolder(overpaidAmount)); Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney)); Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero)); @@ -165,7 +167,8 @@ public void chargePaymentTransactionTestWithLessTransactionAmount() { Mockito.when(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney); Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false); - underTest.processLatestTransaction(loanTransaction, currency, List.of(installment), Set.of(charge), overpaidAmount); + underTest.processLatestTransaction(loanTransaction, currency, List.of(installment), Set.of(charge), + new MoneyHolder(overpaidAmount)); Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney)); Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney), @@ -218,7 +221,8 @@ public void chargePaymentTransactionTestWithMoreTransactionAmount() { Mockito.when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL)); Mockito.when(loanTransaction.isOn(eq(transactionDate))).thenReturn(true); - underTest.processLatestTransaction(loanTransaction, currency, List.of(installment), Set.of(charge), overpaidAmount); + underTest.processLatestTransaction(loanTransaction, currency, List.of(installment), Set.of(charge), + new MoneyHolder(overpaidAmount)); Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney)); Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero)); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackOnPaymentTypeRepaymentTransactionsTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackOnPaymentTypeRepaymentTransactionsTest.java index b92c9893a99..2f6e8121044 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackOnPaymentTypeRepaymentTransactionsTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackOnPaymentTypeRepaymentTransactionsTest.java @@ -54,7 +54,6 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Named; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -65,7 +64,6 @@ public class LoanChargebackOnPaymentTypeRepaymentTransactionsTest { private ResponseSpecification responseSpec; - private ResponseSpecification responseSpecErr400; private ResponseSpecification responseSpecErr503; private RequestSpecification requestSpec; private ClientHelper clientHelper; @@ -77,13 +75,11 @@ public void setup() { this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); - this.responseSpecErr400 = new ResponseSpecBuilder().expectStatusCode(400).build(); this.responseSpecErr503 = new ResponseSpecBuilder().expectStatusCode(503).build(); this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec); } - @Disabled("Issue with Chargeback and Adv Pment Alloc") @ParameterizedTest @MethodSource("loanProductFactory") public void loanTransactionChargebackForPaymentTypeRepaymentTransactionTest(LoanProductTestBuilder loanProductTestBuilder) {