FINERACT-1968: Fix overpayment calculation

This commit is contained in:
Adam Saghy
2024-01-19 14:31:20 +01:00
parent e169b5cd61
commit d56cf85dcd
9 changed files with 100 additions and 93 deletions
@@ -111,6 +111,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanCollateralManagementDa
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; 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.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.ExceedingTrancheCountException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
@@ -797,7 +798,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
final Set<LoanCharge> loanCharges = new HashSet<>(1); final Set<LoanCharge> loanCharges = new HashSet<>(1);
loanCharges.add(charge); loanCharges.add(charge);
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargesPayment, getCurrency(), chargePaymentInstallments, loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargesPayment, getCurrency(), chargePaymentInstallments,
loanCharges, getTotalOverpaidAsMoney()); loanCharges, new MoneyHolder(getTotalOverpaidAsMoney()));
updateLoanSummaryDerivedFields(); updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(chargesPayment.getTransactionDate(), loanLifecycleStateMachine); doPostLoanTransactionChecks(chargesPayment.getTransactionDate(), loanLifecycleStateMachine);
@@ -3321,7 +3322,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
if (isTransactionChronologicallyLatest && adjustedTransaction == null if (isTransactionChronologicallyLatest && adjustedTransaction == null
&& (!reprocess || !this.repaymentScheduleDetail().isInterestRecalculationEnabled()) && !isForeclosure()) { && (!reprocess || !this.repaymentScheduleDetail().isInterestRecalculationEnabled()) && !isForeclosure()) {
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, getCurrency(), loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, getCurrency(),
getRepaymentScheduleInstallments(), getActiveCharges(), getTotalOverpaidAsMoney()); getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()));
reprocess = false; reprocess = false;
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) { if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) {
@@ -3914,7 +3915,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
addLoanTransaction(loanTransaction); addLoanTransaction(loanTransaction);
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, loanCurrency(), loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, loanCurrency(),
getRepaymentScheduleInstallments(), getActiveCharges(), getTotalOverpaidAsMoney()); getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()));
updateLoanSummaryDerivedFields(); updateLoanSummaryDerivedFields();
} }
@@ -4019,7 +4020,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
addLoanTransaction(loanTransaction); addLoanTransaction(loanTransaction);
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, loanCurrency(), loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, loanCurrency(),
getRepaymentScheduleInstallments(), getActiveCharges(), getTotalOverpaidAsMoney()); getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()));
updateLoanSummaryDerivedFields(); updateLoanSummaryDerivedFields();
} else if (totalOutstanding.isGreaterThanZero()) { } else if (totalOutstanding.isGreaterThanZero()) {
@@ -6373,7 +6374,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
// If is a refund // If is a refund
if (adjustedTransaction == null) { if (adjustedTransaction == null) {
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, getCurrency(), loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, getCurrency(),
getRepaymentScheduleInstallments(), getActiveCharges(), getTotalOverpaidAsMoney()); getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()));
} else { } else {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement(); final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
@@ -6404,7 +6405,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
addLoanTransaction(chargebackTransaction); addLoanTransaction(chargebackTransaction);
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargebackTransaction, getCurrency(), loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargebackTransaction, getCurrency(),
getRepaymentScheduleInstallments(), getActiveCharges(), getTotalOverpaidAsMoney()); getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()));
updateLoanSummaryDerivedFields(); updateLoanSummaryDerivedFields();
if (!doPostLoanTransactionChecks(chargebackTransaction.getTransactionDate(), loanLifecycleStateMachine)) { if (!doPostLoanTransactionChecks(chargebackTransaction.getTransactionDate(), loanLifecycleStateMachine)) {
@@ -483,11 +483,6 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom {
.plus(getPenaltyChargesPortion(currency)).getAmount(); .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) { public void setOverPayments(final Money overPayment) {
if (overPayment != null) { if (overPayment != null) {
this.overPaymentPortion = defaultToNullIfZero(overPayment.getAmount()); this.overPaymentPortion = defaultToNullIfZero(overPayment.getAmount());
@@ -144,7 +144,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
if (unprocessed.isGreaterThanZero()) { if (unprocessed.isGreaterThanZero()) {
onLoanOverpayment(loanTransaction, unprocessed); onLoanOverpayment(loanTransaction, unprocessed);
loanTransaction.updateOverPayments(unprocessed); loanTransaction.setOverPayments(unprocessed);
} }
} else { } else {
@@ -152,6 +152,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
} }
} }
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(currency));
for (final LoanTransaction loanTransaction : transactionsToBeProcessed) { for (final LoanTransaction loanTransaction : transactionsToBeProcessed) {
// TODO: analyze and remove this // TODO: analyze and remove this
if (!loanTransaction.getTypeOf().equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN)) { if (!loanTransaction.getTypeOf().equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN)) {
@@ -163,7 +164,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
// pass through for new transactions // pass through for new transactions
if (loanTransaction.getId() == null) { if (loanTransaction.getId() == null) {
processLatestTransaction(loanTransaction, currency, installments, charges, null); processLatestTransaction(loanTransaction, currency, installments, charges, overpaymentHolder);
loanTransaction.adjustInterestComponent(currency); loanTransaction.adjustInterestComponent(currency);
} else { } else {
/** /**
@@ -174,7 +175,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
// Reset derived component of new loan transaction and // Reset derived component of new loan transaction and
// re-process transaction // re-process transaction
processLatestTransaction(newLoanTransaction, currency, installments, charges, null); processLatestTransaction(newLoanTransaction, currency, installments, charges, overpaymentHolder);
newLoanTransaction.adjustInterestComponent(currency); newLoanTransaction.adjustInterestComponent(currency);
/** /**
* Check if the transaction amounts have changed. If so, reverse the original transaction and update * Check if the transaction amounts have changed. If so, reverse the original transaction and update
@@ -195,9 +196,11 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
loanTransaction.resetDerivedComponents(); loanTransaction.resetDerivedComponents();
handleRefund(loanTransaction, currency, installments, charges); handleRefund(loanTransaction, currency, installments, charges);
} else if (loanTransaction.isCreditBalanceRefund()) { } else if (loanTransaction.isCreditBalanceRefund()) {
recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed); recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed,
overpaymentHolder);
} else if (loanTransaction.isChargeback()) { } else if (loanTransaction.isChargeback()) {
recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed); recalculateCreditTransaction(changedTransactionDetail, loanTransaction, currency, installments, transactionsToBeProcessed,
overpaymentHolder);
reprocessChargebackTransactionRelation(changedTransactionDetail, transactionsToBeProcessed); reprocessChargebackTransactionRelation(changedTransactionDetail, transactionsToBeProcessed);
} else if (loanTransaction.isChargeOff()) { } else if (loanTransaction.isChargeOff()) {
recalculateChargeOffTransaction(changedTransactionDetail, loanTransaction, currency, installments); recalculateChargeOffTransaction(changedTransactionDetail, loanTransaction, currency, installments);
@@ -209,11 +212,11 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
@Override @Override
public void processLatestTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency, public void processLatestTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, Money overpaidAmount) { final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, MoneyHolder overpaymentHolder) {
switch (loanTransaction.getTypeOf()) { switch (loanTransaction.getTypeOf()) {
case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments); case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments);
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges); 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 -> { default -> {
Money transactionAmountUnprocessed = handleTransactionAndCharges(loanTransaction, currency, installments, charges, null, Money transactionAmountUnprocessed = handleTransactionAndCharges(loanTransaction, currency, installments, charges, null,
false); false);
@@ -223,8 +226,11 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
transactionAmountUnprocessed.zero(), transactionAmountUnprocessed.zero()); transactionAmountUnprocessed.zero(), transactionAmountUnprocessed.zero());
} else { } else {
onLoanOverpayment(loanTransaction, transactionAmountUnprocessed); onLoanOverpayment(loanTransaction, transactionAmountUnprocessed);
loanTransaction.updateOverPayments(transactionAmountUnprocessed); loanTransaction.setOverPayments(transactionAmountUnprocessed);
} }
overpaymentHolder.setMoneyObject(transactionAmountUnprocessed);
} else {
overpaymentHolder.setMoneyObject(Money.zero(currency));
} }
} }
} }
@@ -435,17 +441,15 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
} }
private void recalculateCreditTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction loanTransaction, private void recalculateCreditTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction loanTransaction,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, List<LoanTransaction> transactionsToBeProcessed,
List<LoanTransaction> transactionsToBeProcessed) { MoneyHolder overpaymentHolder) {
// pass through for new transactions // pass through for new transactions
if (loanTransaction.getId() == null) { if (loanTransaction.getId() == null) {
return; return;
} }
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
List<LoanTransaction> mergedList = getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail); processCreditTransaction(newLoanTransaction, overpaymentHolder, currency, installments);
Money overpaidAmount = calculateOverpaidAmount(loanTransaction, mergedList, installments, currency);
processCreditTransaction(newLoanTransaction, overpaidAmount, currency, installments);
if (!LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) { if (!LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) {
createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail); createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail);
} }
@@ -470,40 +474,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
} }
private Money calculateOverpaidAmount(LoanTransaction loanTransaction, List<LoanTransaction> transactions, private void processCreditTransaction(LoanTransaction loanTransaction, MoneyHolder overpaymentHolder, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> 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,
List<LoanRepaymentScheduleInstallment> installments) { List<LoanRepaymentScheduleInstallment> installments) {
loanTransaction.resetDerivedComponents(); loanTransaction.resetDerivedComponents();
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>(); List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
@@ -511,9 +482,10 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
installments.sort(byDate); installments.sort(byDate);
final Money zeroMoney = Money.zero(currency); final Money zeroMoney = Money.zero(currency);
Money transactionAmount = loanTransaction.getAmount(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)); 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); loanTransaction.updateComponents(principalPortion, zeroMoney, zeroMoney, zeroMoney);
if (principalPortion.isGreaterThanZero()) { if (principalPortion.isGreaterThanZero()) {
@@ -770,14 +742,14 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion); loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
} }
protected void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount, protected void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments) { List<LoanRepaymentScheduleInstallment> installments, MoneyHolder overpaidAmountHolder) {
processCreditTransaction(loanTransaction, overpaidAmount, currency, installments); processCreditTransaction(loanTransaction, overpaidAmountHolder, currency, installments);
} }
protected void handleCreditBalanceRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount, protected void handleCreditBalanceRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments) { List<LoanRepaymentScheduleInstallment> installments, MoneyHolder overpaidAmountHolder) {
processCreditTransaction(loanTransaction, overpaidAmount, currency, installments); processCreditTransaction(loanTransaction, overpaidAmountHolder, currency, installments);
} }
protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
@@ -41,7 +41,7 @@ public interface LoanRepaymentScheduleTransactionProcessor {
* schedule. * schedule.
*/ */
void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, Money overpaidAmount); List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, MoneyHolder overpaymentHolder);
/** /**
* Provides support for passing all {@link LoanTransaction}'s so it will completely re-process the entire loan * Provides support for passing all {@link LoanTransaction}'s so it will completely re-process the entire loan
@@ -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;
}
@@ -55,6 +55,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper; 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.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanproduct.domain.DueType; import org.apache.fineract.portfolio.loanproduct.domain.DueType;
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
@@ -136,9 +137,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
List<ChargeOrTransaction> chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges); List<ChargeOrTransaction> chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges);
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(currency));
for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) { for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) {
chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, currency, chargeOrTransaction.getLoanTransaction().ifPresent(loanTransaction -> processSingleTransaction(loanTransaction, currency,
installments, charges, changedTransactionDetail)); installments, charges, changedTransactionDetail, overpaymentHolder));
chargeOrTransaction.getLoanCharge() chargeOrTransaction.getLoanCharge()
.ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate)); .ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate));
} }
@@ -150,18 +152,18 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
@Override @Override
public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, Money overpaidAmount) { List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, MoneyHolder overpaymentHolder) {
switch (loanTransaction.getTypeOf()) { switch (loanTransaction.getTypeOf()) {
case DISBURSEMENT -> handleDisbursement(loanTransaction, currency, installments); case DISBURSEMENT -> handleDisbursement(loanTransaction, currency, installments);
case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments); case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments);
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges); case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges);
case CHARGEBACK -> handleChargeback(loanTransaction, currency, overpaidAmount, installments); case CHARGEBACK -> handleChargeback(loanTransaction, currency, installments, overpaymentHolder);
case CREDIT_BALANCE_REFUND -> handleCreditBalanceRefund(loanTransaction, currency, overpaidAmount, installments); case CREDIT_BALANCE_REFUND -> handleCreditBalanceRefund(loanTransaction, currency, installments, overpaymentHolder);
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT, case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
WAIVE_INTEREST, RECOVERY_REPAYMENT -> WAIVE_INTEREST, RECOVERY_REPAYMENT ->
handleRepayment(loanTransaction, currency, installments, charges); handleRepayment(loanTransaction, currency, installments, charges, overpaymentHolder);
case CHARGE_OFF -> handleChargeOff(loanTransaction, currency, installments); 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."); case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed.");
// TODO: Cover rest of the transaction types // TODO: Cover rest of the transaction types
default -> { default -> {
@@ -226,10 +228,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, private void processSingleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, ChangedTransactionDetail changedTransactionDetail,
ChangedTransactionDetail changedTransactionDetail) { MoneyHolder overpaymentHolder) {
if (loanTransaction.getId() == null) { if (loanTransaction.getId() == null) {
processLatestTransaction(loanTransaction, currency, installments, charges, Money.zero(currency)); processLatestTransaction(loanTransaction, currency, installments, charges, overpaymentHolder);
if (loanTransaction.isInterestWaiver()) { if (loanTransaction.isInterestWaiver()) {
loanTransaction.adjustInterestComponent(currency); loanTransaction.adjustInterestComponent(currency);
} }
@@ -242,7 +244,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
// Reset derived component of new loan transaction and // Reset derived component of new loan transaction and
// re-process transaction // re-process transaction
processLatestTransaction(newLoanTransaction, currency, installments, charges, Money.zero(currency)); processLatestTransaction(newLoanTransaction, currency, installments, charges, overpaymentHolder);
if (loanTransaction.isInterestWaiver()) { if (loanTransaction.isInterestWaiver()) {
newLoanTransaction.adjustInterestComponent(currency); newLoanTransaction.adjustInterestComponent(currency);
} }
@@ -322,12 +324,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
private void handleRepayment(LoanTransaction loanTransaction, MonetaryCurrency currency, private void handleRepayment(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) { List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, MoneyHolder overpaymentHolder) {
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
loanTransaction.resetDerivedComponents(); loanTransaction.resetDerivedComponents();
} }
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency); Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
processTransaction(loanTransaction, currency, installments, transactionAmountUnprocessed, charges); processTransaction(loanTransaction, currency, installments, transactionAmountUnprocessed, charges, overpaymentHolder);
} }
private LoanTransactionToRepaymentScheduleMapping getTransactionMapping( private LoanTransactionToRepaymentScheduleMapping getTransactionMapping(
@@ -399,10 +401,13 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
loanTransactionToRepaymentScheduleMapping.setComponents(aggregatedPrincipal, aggregatedInterest, aggregatedFee, aggregatedPenalty); 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()) { if (overpaymentPortion.isGreaterThanZero()) {
onLoanOverpayment(loanTransaction, overpaymentPortion); onLoanOverpayment(loanTransaction, overpaymentPortion);
loanTransaction.updateOverPayments(overpaymentPortion); overpaymentHolder.setMoneyObject(overpaymentPortion);
loanTransaction.setOverPayments(overpaymentPortion);
} else {
overpaymentHolder.setMoneyObject(overpaymentPortion.zero());
} }
} }
@@ -427,7 +432,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
private void handleChargePayment(LoanTransaction loanTransaction, MonetaryCurrency currency, private void handleChargePayment(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) { List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, MoneyHolder overpaymentHolder) {
Money zero = Money.zero(currency); Money zero = Money.zero(currency);
Money feeChargesPortion = zero; Money feeChargesPortion = zero;
Money penaltyChargesPortion = zero; Money penaltyChargesPortion = zero;
@@ -473,7 +478,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
if (unprocessed.isGreaterThanZero()) { if (unprocessed.isGreaterThanZero()) {
processTransaction(loanTransaction, currency, installments, unprocessed, charges); processTransaction(loanTransaction, currency, installments, unprocessed, charges, overpaymentHolder);
} }
} }
@@ -658,7 +663,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
private void processTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, private void processTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed, Set<LoanCharge> charges) { List<LoanRepaymentScheduleInstallment> installments, Money transactionAmountUnprocessed, Set<LoanCharge> charges,
MoneyHolder overpaymentHolder) {
Money zero = Money.zero(currency); Money zero = Money.zero(currency);
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>(); List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
@@ -683,7 +689,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion()); balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
handleOverpayment(transactionAmountUnprocessed, loanTransaction); handleOverpayment(transactionAmountUnprocessed, loanTransaction, overpaymentHolder);
} }
private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency, private Money processPeriodsHorizontally(LoanTransaction loanTransaction, MonetaryCurrency currency,
@@ -114,6 +114,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationT
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; 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.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.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException; import org.apache.fineract.portfolio.loanaccount.exception.InstallmentNotFoundException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException;
@@ -832,7 +833,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
.determineProcessor(loan.transactionProcessingStrategy()); .determineProcessor(loan.transactionProcessingStrategy());
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanChargeAdjustmentTransaction, loan.getCurrency(), loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanChargeAdjustmentTransaction, loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), loan.getTotalOverpaidAsMoney()); loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()));
loan.addLoanTransaction(loanChargeAdjustmentTransaction); loan.addLoanTransaction(loanChargeAdjustmentTransaction);
loan.updateLoanSummaryAndStatus(); loan.updateLoanSummaryAndStatus();
@@ -43,6 +43,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRul
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; 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.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
@@ -121,7 +122,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Mockito.when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney); Mockito.when(charge.updatePaidAmountBy(refEq(chargeAmountMoney), eq(1), refEq(zero))).thenReturn(chargeAmountMoney);
Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false); 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(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero)); Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
@@ -165,7 +167,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Mockito.when(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney); Mockito.when(charge.updatePaidAmountBy(refEq(transactionAmountMoney), eq(1), refEq(zero))).thenReturn(transactionAmountMoney);
Mockito.when(loanTransaction.isPenaltyPayment()).thenReturn(false); 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(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(transactionAmountMoney));
Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney), Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(transactionAmountMoney),
@@ -218,7 +221,8 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
Mockito.when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL)); Mockito.when(loanPaymentAllocationRule.getAllocationTypes()).thenReturn(List.of(PaymentAllocationType.DUE_PRINCIPAL));
Mockito.when(loanTransaction.isOn(eq(transactionDate))).thenReturn(true); 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(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), eq(chargeAmountMoney));
Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero)); Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(chargeAmountMoney), refEq(zero));
@@ -54,7 +54,6 @@ import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHe
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
@@ -65,7 +64,6 @@ import org.junit.jupiter.params.provider.MethodSource;
public class LoanChargebackOnPaymentTypeRepaymentTransactionsTest { public class LoanChargebackOnPaymentTypeRepaymentTransactionsTest {
private ResponseSpecification responseSpec; private ResponseSpecification responseSpec;
private ResponseSpecification responseSpecErr400;
private ResponseSpecification responseSpecErr503; private ResponseSpecification responseSpecErr503;
private RequestSpecification requestSpec; private RequestSpecification requestSpec;
private ClientHelper clientHelper; private ClientHelper clientHelper;
@@ -77,13 +75,11 @@ public class LoanChargebackOnPaymentTypeRepaymentTransactionsTest {
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.responseSpecErr400 = new ResponseSpecBuilder().expectStatusCode(400).build();
this.responseSpecErr503 = new ResponseSpecBuilder().expectStatusCode(503).build(); this.responseSpecErr503 = new ResponseSpecBuilder().expectStatusCode(503).build();
this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec); this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec);
} }
@Disabled("Issue with Chargeback and Adv Pment Alloc")
@ParameterizedTest @ParameterizedTest
@MethodSource("loanProductFactory") @MethodSource("loanProductFactory")
public void loanTransactionChargebackForPaymentTypeRepaymentTransactionTest(LoanProductTestBuilder loanProductTestBuilder) { public void loanTransactionChargebackForPaymentTypeRepaymentTransactionTest(LoanProductTestBuilder loanProductTestBuilder) {