This is an automated email from the ASF dual-hosted git repository.
arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 0a2db35f8 FINERACT-1839: Reversal on the loan account after CBR /
Chargeback
0a2db35f8 is described below
commit 0a2db35f8d791cf3d31b20a53862c99b82a4fbde
Author: Adam Saghy <[email protected]>
AuthorDate: Thu Feb 2 20:34:03 2023 +0100
FINERACT-1839: Reversal on the loan account after CBR / Chargeback
---
.../service/LoanDelinquencyDomainServiceImpl.java | 68 +++---
.../portfolio/loanaccount/domain/Loan.java | 22 +-
.../domain/LoanRepaymentScheduleInstallment.java | 6 +-
.../loanaccount/domain/LoanTransaction.java | 5 +-
...tLoanRepaymentScheduleTransactionProcessor.java | 267 +++++++++++++++------
.../LoanWritePlatformServiceJpaRepositoryImpl.java | 3 +-
.../LoanDelinquencyDomainServiceTest.java | 4 +
.../ClientLoanIntegrationTest.java | 229 ++++++++++++++++++
8 files changed, 492 insertions(+), 112 deletions(-)
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
index 1d569c90b..72086c72f 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/LoanDelinquencyDomainServiceImpl.java
@@ -20,6 +20,8 @@ package org.apache.fineract.portfolio.delinquency.service;
import java.math.BigDecimal;
import java.time.LocalDate;
+import java.util.List;
+import java.util.Objects;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
@@ -27,7 +29,6 @@ import
org.apache.fineract.portfolio.loanaccount.data.CollectionData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
-import
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -43,17 +44,19 @@ public class LoanDelinquencyDomainServiceImpl implements
LoanDelinquencyDomainSe
final MonetaryCurrency loanCurrency = loan.getCurrency();
LocalDate overdueSinceDate = null;
CollectionData collectionData = CollectionData.template();
- BigDecimal amountAvailable = BigDecimal.ZERO;
+ BigDecimal amountAvailable;
BigDecimal outstandingAmount = BigDecimal.ZERO;
boolean oldestOverdueInstallment = false;
boolean overdueSinceDateWasSet = false;
boolean firstNotYetDueInstallment = false;
+ LoanRepaymentScheduleInstallment latestInstallment =
loan.getLastLoanRepaymentScheduleInstallment();
+
+ List<LoanTransaction> chargebackTransactions =
loan.getLoanTransactions(LoanTransaction::isChargeback);
log.debug("Loan id {} with {} installments", loan.getId(),
loan.getRepaymentScheduleInstallments().size());
// Get the oldest overdue installment if exists one
for (LoanRepaymentScheduleInstallment installment :
loan.getRepaymentScheduleInstallments()) {
if (!installment.isObligationsMet()) {
-
if (installment.getDueDate().isBefore(businessDate)) {
log.debug("Loan Id: {} with installment {} due date {}",
loan.getId(), installment.getInstallmentNumber(),
installment.getDueDate());
@@ -66,10 +69,17 @@ public class LoanDelinquencyDomainServiceImpl implements
LoanDelinquencyDomainSe
amountAvailable =
installment.getTotalPaid(loanCurrency).getAmount();
- for (LoanTransactionToRepaymentScheduleMapping
mappingInstallment : installment
-
.getLoanTransactionToRepaymentScheduleMappings()) {
- final LoanTransaction loanTransaction =
mappingInstallment.getLoanTransaction();
- if (loanTransaction.isChargeback()) {
+ boolean isLatestInstallment =
Objects.equals(installment.getId(), latestInstallment.getId());
+ for (LoanTransaction loanTransaction :
chargebackTransactions) {
+ boolean
isLoanTransactionIsOnOrAfterInstallmentFromDate =
loanTransaction.getTransactionDate().isEqual(
+ installment.getFromDate()) ||
loanTransaction.getTransactionDate().isAfter(installment.getFromDate());
+ boolean
isLoanTransactionIsBeforeNotLastInstallmentDueDate = !isLatestInstallment
+ &&
loanTransaction.getTransactionDate().isBefore(installment.getDueDate());
+ boolean
isLoanTransactionIsOnOrBeforeLastInstallmentDueDate = isLatestInstallment
+ &&
(loanTransaction.getTransactionDate().isEqual(installment.getDueDate())
+ ||
loanTransaction.getTransactionDate().isBefore(installment.getDueDate()));
+ if
(isLoanTransactionIsOnOrAfterInstallmentFromDate &&
(isLoanTransactionIsBeforeNotLastInstallmentDueDate
+ ||
isLoanTransactionIsOnOrBeforeLastInstallmentDueDate)) {
amountAvailable =
amountAvailable.subtract(loanTransaction.getAmount());
if (amountAvailable.compareTo(BigDecimal.ZERO)
< 0) {
overdueSinceDate =
loanTransaction.getTransactionDate();
@@ -78,29 +88,33 @@ public class LoanDelinquencyDomainServiceImpl implements
LoanDelinquencyDomainSe
}
}
}
- }
- } else if (!firstNotYetDueInstallment) {
- log.debug("Loan Id: {} with installment {} due date {}",
loan.getId(), installment.getInstallmentNumber(),
- installment.getDueDate());
- firstNotYetDueInstallment = true;
- amountAvailable =
installment.getTotalPaid(loanCurrency).getAmount();
- log.debug("Amount available {}", amountAvailable);
- for (LoanTransactionToRepaymentScheduleMapping
mappingInstallment : installment
- .getLoanTransactionToRepaymentScheduleMappings()) {
- final LoanTransaction loanTransaction =
mappingInstallment.getLoanTransaction();
- if (loanTransaction.isChargeback() &&
loanTransaction.getTransactionDate().isBefore(businessDate)) {
- log.debug("Loan CB Transaction: {} {} {}",
loanTransaction.getId(), loanTransaction.getTransactionDate(),
- loanTransaction.getAmount());
- amountAvailable =
amountAvailable.subtract(loanTransaction.getAmount());
- if (amountAvailable.compareTo(BigDecimal.ZERO) < 0 &&
!overdueSinceDateWasSet) {
- overdueSinceDate =
loanTransaction.getTransactionDate();
- overdueSinceDateWasSet = true;
+ } else if (!firstNotYetDueInstallment) {
+ log.debug("Loan Id: {} with installment {} due date {}",
loan.getId(), installment.getInstallmentNumber(),
+ installment.getDueDate());
+ firstNotYetDueInstallment = true;
+ amountAvailable =
installment.getTotalPaid(loanCurrency).getAmount();
+ log.debug("Amount available {}", amountAvailable);
+ for (LoanTransaction loanTransaction :
chargebackTransactions) {
+ boolean
isLoanTransactionIsOnOrAfterInstallmentFromDate =
loanTransaction.getTransactionDate().isEqual(
+ installment.getFromDate()) ||
loanTransaction.getTransactionDate().isAfter(installment.getFromDate());
+ boolean isLoanTransactionIsBeforeInstallmentDueDate =
loanTransaction.getTransactionDate()
+ .isBefore(installment.getDueDate());
+ boolean isLoanTransactionIsBeforeBusinessDate =
loanTransaction.getTransactionDate().isBefore(businessDate);
+ if (isLoanTransactionIsOnOrAfterInstallmentFromDate &&
isLoanTransactionIsBeforeInstallmentDueDate
+ && isLoanTransactionIsBeforeBusinessDate) {
+ log.debug("Loan CB Transaction: {} {} {}",
loanTransaction.getId(), loanTransaction.getTransactionDate(),
+ loanTransaction.getAmount());
+ amountAvailable =
amountAvailable.subtract(loanTransaction.getAmount());
+ if (amountAvailable.compareTo(BigDecimal.ZERO) < 0
&& !overdueSinceDateWasSet) {
+ overdueSinceDate =
loanTransaction.getTransactionDate();
+ overdueSinceDateWasSet = true;
+ }
}
}
- }
- if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) {
- outstandingAmount =
outstandingAmount.add(amountAvailable.abs());
+ if (amountAvailable.compareTo(BigDecimal.ZERO) < 0) {
+ outstandingAmount =
outstandingAmount.add(amountAvailable.abs());
+ }
}
}
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index e9ccab2fd..a1307afc2 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -44,6 +44,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.function.Predicate;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Embedded;
@@ -1346,8 +1347,7 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
}
installment.updateAccrualPortion(interest, fee, penality);
}
- LoanRepaymentScheduleInstallment lastInstallment =
getRepaymentScheduleInstallments()
- .get(getRepaymentScheduleInstallments().size() - 1);
+ LoanRepaymentScheduleInstallment lastInstallment =
getLastLoanRepaymentScheduleInstallment();
for (LoanTransaction loanTransaction : accruals) {
if
(loanTransaction.getTransactionDate().isAfter(lastInstallment.getDueDate()) &&
!loanTransaction.isReversed()) {
loanTransaction.reverse();
@@ -3798,9 +3798,10 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
if (loanTransaction.isReversed()) {
continue;
}
- if ((loanTransaction.isRefund() ||
loanTransaction.isRefundForActiveLoan() ||
loanTransaction.isCreditBalanceRefund()
- || loanTransaction.isChargeback())) {
+ if (loanTransaction.isRefund() ||
loanTransaction.isRefundForActiveLoan()) {
totalPaidInRepayments =
totalPaidInRepayments.minus(loanTransaction.getAmount(currency));
+ } else if (loanTransaction.isCreditBalanceRefund() ||
loanTransaction.isChargeback()) {
+ totalPaidInRepayments =
totalPaidInRepayments.minus(loanTransaction.getOverPaymentPortion(currency));
}
}
@@ -5804,7 +5805,7 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
if (loanTransaction.isDisbursement() ||
loanTransaction.isIncomePosting()) {
outstanding =
outstanding.plus(loanTransaction.getAmount(getCurrency()));
loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount());
- } else if (loanTransaction.isChargeback()) {
+ } else if (loanTransaction.isChargeback() ||
loanTransaction.isCreditBalanceRefund()) {
Money transactionOutstanding =
loanTransaction.getAmount(getCurrency());
if
(!loanTransaction.getOverPaymentPortion(getCurrency()).isZero()) {
transactionOutstanding =
loanTransaction.getAmount(getCurrency())
@@ -6277,9 +6278,6 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
final LoanRepaymentScheduleTransactionProcessor
loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
final Money overpaidAmount = calculateTotalOverpayment(); // Before
Transaction
- if (overpaidAmount.isGreaterThanZero()) {
- chargebackTransaction.setOverPayments(overpaidAmount);
- }
if (chargebackTransaction.isNotZero(loanCurrency())) {
addLoanTransaction(chargebackTransaction);
@@ -7018,4 +7016,12 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
return this.chargedOff;
}
+ public LoanRepaymentScheduleInstallment
getLastLoanRepaymentScheduleInstallment() {
+ return
getRepaymentScheduleInstallments().get(getRepaymentScheduleInstallments().size()
- 1);
+ }
+
+ public List<LoanTransaction>
getLoanTransactions(Predicate<LoanTransaction> predicate) {
+ return getLoanTransactions().stream().filter(predicate).toList();
+ }
+
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
index 9e864bb51..ab2b6326d 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java
@@ -393,6 +393,10 @@ public class LoanRepaymentScheduleInstallment extends
AbstractAuditableWithUTCDa
this.obligationsMet = false;
this.obligationsMetOnDate = null;
+ if (this.credits != null) {
+ this.principal = this.principal.subtract(this.credits);
+ this.credits = null;
+ }
}
public void resetAccrualComponents() {
@@ -836,7 +840,7 @@ public class LoanRepaymentScheduleInstallment extends
AbstractAuditableWithUTCDa
}
}
- public void updateDueChargeback(final LocalDate transactionDate, final
Money transactionAmount) {
+ public void updateDueAndCredits(final LocalDate transactionDate, final
Money transactionAmount) {
updateDueDate(transactionDate);
addToCredits(transactionAmount.getAmount());
addToPrincipal(transactionDate, transactionAmount);
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
index 9a0fbcc60..2ae376090 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
@@ -291,9 +291,8 @@ public class LoanTransaction extends
AbstractAuditableWithUTCDateTimeCustom {
public static LoanTransaction creditBalanceRefund(final Loan loan, final
Office office, final Money amount, final LocalDate paymentDate,
final ExternalId externalId) {
- final PaymentDetail paymentDetail = null;
- return new LoanTransaction(loan, office,
LoanTransactionType.CREDIT_BALANCE_REFUND, paymentDetail, amount.getAmount(),
paymentDate,
- externalId);
+ return new LoanTransaction(loan, office,
LoanTransactionType.CREDIT_BALANCE_REFUND.getValue(), paymentDate,
amount.getAmount(),
+ null, null, null, null, amount.getAmount(), false, null,
externalId);
}
public static LoanTransaction refundForActiveLoan(final Office office,
final Money amount, final PaymentDetail paymentDetail,
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
index ecb992846..46d79637a 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
@@ -25,7 +25,9 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
+import java.util.Map;
import java.util.Set;
+import org.apache.fineract.interoperation.util.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidDetail;
@@ -34,7 +36,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge;
-import
org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
@@ -93,7 +94,7 @@ public abstract class
AbstractLoanRepaymentScheduleTransactionProcessor implemen
wrapper.reprocess(currency, disbursementDate, installments, charges);
final ChangedTransactionDetail changedTransactionDetail = new
ChangedTransactionDetail();
- final List<LoanTransaction> transactionstoBeProcessed = new
ArrayList<>();
+ final List<LoanTransaction> transactionsToBeProcessed = new
ArrayList<>();
for (final LoanTransaction loanTransaction :
transactionsPostDisbursement) {
if (loanTransaction.isChargePayment()) {
List<LoanChargePaidDetail> chargePaidDetails = new
ArrayList<>();
@@ -147,11 +148,11 @@ public abstract class
AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
} else {
- transactionstoBeProcessed.add(loanTransaction);
+ transactionsToBeProcessed.add(loanTransaction);
}
}
- for (final LoanTransaction loanTransaction :
transactionstoBeProcessed) {
+ for (final LoanTransaction loanTransaction :
transactionsToBeProcessed) {
// TODO: analyze and remove this
if
(!loanTransaction.getTypeOf().equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN))
{
final Comparator<LoanRepaymentScheduleInstallment> byDate =
new Comparator<LoanRepaymentScheduleInstallment>() {
@@ -188,13 +189,7 @@ public abstract class
AbstractLoanRepaymentScheduleTransactionProcessor implemen
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
} else {
- loanTransaction.reverse();
- loanTransaction.updateExternalId(null);
-
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
- // Adding Replayed relation from newly created
transaction to reversed transaction
-
newLoanTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(newLoanTransaction,
- loanTransaction,
LoanTransactionRelationTypeEnum.REPLAYED));
-
changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(),
newLoanTransaction);
+ createNewTransactionIfNecessary(loanTransaction,
newLoanTransaction, currency, changedTransactionDetail);
}
}
@@ -203,13 +198,200 @@ public abstract class
AbstractLoanRepaymentScheduleTransactionProcessor implemen
handleWriteOff(loanTransaction, currency, installments);
} else if (loanTransaction.isRefundForActiveLoan()) {
loanTransaction.resetDerivedComponents();
-
handleRefund(loanTransaction, currency, installments, charges);
+ } else if (loanTransaction.isCreditBalanceRefund()) {
+ recalculateCreditTransaction(changedTransactionDetail,
loanTransaction, currency, installments, transactionsToBeProcessed);
+ } else if (loanTransaction.isChargeback()) {
+ recalculateCreditTransaction(changedTransactionDetail,
loanTransaction, currency, installments, transactionsToBeProcessed);
+
reprocessChargebackTransactionRelation(changedTransactionDetail,
transactionsToBeProcessed);
}
}
+ reprocessInstallments(installments, currency);
+
return changedTransactionDetail;
}
+ private void
reprocessChargebackTransactionRelation(ChangedTransactionDetail
changedTransactionDetail,
+ List<LoanTransaction> transactionsToBeProcessed) {
+
+ List<LoanTransaction> mergedTransactionList =
getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail);
+ for (Map.Entry<Long, LoanTransaction> entry :
changedTransactionDetail.getNewTransactionMappings().entrySet()) {
+ if (entry.getValue().isChargeback()) {
+ for (LoanTransaction loanTransaction : mergedTransactionList) {
+ if (loanTransaction.isReversed()) {
+ continue;
+ }
+ LoanTransactionRelation newLoanTransactionRelation = null;
+ LoanTransactionRelation oldLoanTransactionRelation = null;
+ for (LoanTransactionRelation transactionRelation :
loanTransaction.getLoanTransactionRelations()) {
+ if
(entry.getKey().equals(transactionRelation.getToTransaction().getId())
+ &&
LoanTransactionRelationTypeEnum.CHARGEBACK.equals(transactionRelation.getRelationType()))
{
+ newLoanTransactionRelation =
LoanTransactionRelation.linkToTransaction(loanTransaction, entry.getValue(),
+
LoanTransactionRelationTypeEnum.CHARGEBACK);
+ oldLoanTransactionRelation = transactionRelation;
+ break;
+ }
+ }
+ if (newLoanTransactionRelation != null) {
+
loanTransaction.getLoanTransactionRelations().add(newLoanTransactionRelation);
+
loanTransaction.getLoanTransactionRelations().remove(oldLoanTransactionRelation);
+ }
+ }
+ }
+ }
+ }
+
+ private void reprocessInstallments(List<LoanRepaymentScheduleInstallment>
installments, MonetaryCurrency currency) {
+ LoanRepaymentScheduleInstallment lastInstallment =
installments.get(installments.size() - 1);
+ if (lastInstallment.isAdditional() &&
lastInstallment.getDue(currency).isZero()) {
+ installments.remove(lastInstallment);
+ }
+ }
+
+ private void recalculateCreditTransaction(ChangedTransactionDetail
changedTransactionDetail, LoanTransaction loanTransaction,
+ MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment>
installments,
+ List<LoanTransaction> transactionsToBeProcessed) {
+ // pass through for new transactions
+ if (loanTransaction.getId() == null) {
+ return;
+ }
+ final LoanTransaction newLoanTransaction =
LoanTransaction.copyTransactionProperties(loanTransaction);
+
+ List<LoanTransaction> mergedList =
getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail);
+ Money overpaidAmount = calculateOverpaidAmount(loanTransaction,
mergedList, installments, currency);
+ processCreditTransaction(newLoanTransaction, overpaidAmount, currency,
installments);
+ createNewTransactionIfNecessary(loanTransaction, newLoanTransaction,
currency, changedTransactionDetail);
+ }
+
+ private List<LoanTransaction>
getMergedTransactionList(List<LoanTransaction> transactionList,
+ ChangedTransactionDetail changedTransactionDetail) {
+ List<LoanTransaction> mergedList = new
ArrayList<>(changedTransactionDetail.getNewTransactionMappings().values());
+ mergedList.addAll(new ArrayList<>(transactionList));
+ return mergedList;
+ }
+
+ private void createNewTransactionIfNecessary(LoanTransaction
loanTransaction, LoanTransaction newLoanTransaction,
+ MonetaryCurrency currency, ChangedTransactionDetail
changedTransactionDetail) {
+ if (!LoanTransaction.transactionAmountsMatch(currency,
loanTransaction, newLoanTransaction)) {
+ loanTransaction.reverse();
+ loanTransaction.updateExternalId(null);
+
newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations());
+ // Adding Replayed relation from newly created transaction to
reversed transaction
+
newLoanTransaction.getLoanTransactionRelations().add(LoanTransactionRelation.linkToTransaction(newLoanTransaction,
+ loanTransaction,
LoanTransactionRelationTypeEnum.REPLAYED));
+
changedTransactionDetail.getNewTransactionMappings().put(loanTransaction.getId(),
newLoanTransaction);
+ }
+ }
+
+ private Money calculateOverpaidAmount(LoanTransaction loanTransaction,
List<LoanTransaction> transactions,
+ 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.isRepaymentType()) {
+ 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) {
+ loanTransaction.resetDerivedComponents();
+ List<LoanTransactionToRepaymentScheduleMapping> transactionMappings =
new ArrayList<>();
+ final Comparator<LoanRepaymentScheduleInstallment> byDate =
Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
+ installments.sort(byDate);
+ final Money zeroMoney = Money.zero(currency);
+ Money transactionAmount = loanTransaction.getAmount(currency);
+ Money principalPortion =
MathUtil.negativeToZero(loanTransaction.getAmount(currency).minus(overpaidAmount));
+ Money repaidAmount =
MathUtil.negativeToZero(transactionAmount.minus(principalPortion));
+ loanTransaction.updateOverPayments(repaidAmount);
+ loanTransaction.updateComponents(principalPortion, zeroMoney,
zeroMoney, zeroMoney);
+
+ if (principalPortion.isGreaterThanZero()) {
+ final LocalDate transactionDate =
loanTransaction.getTransactionDate();
+ boolean loanTransactionMapped = false;
+ LocalDate pastDueDate = null;
+ for (final LoanRepaymentScheduleInstallment currentInstallment :
installments) {
+ pastDueDate = currentInstallment.getDueDate();
+ if (!currentInstallment.isAdditional() &&
currentInstallment.getDueDate().isAfter(transactionDate)) {
+
currentInstallment.addToCredits(transactionAmount.getAmount());
+ currentInstallment.addToPrincipal(transactionDate,
transactionAmount);
+ if (repaidAmount.isGreaterThanZero()) {
+
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(),
repaidAmount);
+
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
currentInstallment,
+ repaidAmount, zeroMoney, zeroMoney,
zeroMoney));
+ }
+ loanTransactionMapped = true;
+ break;
+
+ // If already exists an additional installment just update
the due date and
+ // principal from the Loan charge back transaction
+ } else if (currentInstallment.isAdditional()) {
+ currentInstallment.updateDueAndCredits(transactionDate,
transactionAmount);
+ if (repaidAmount.isGreaterThanZero()) {
+
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(),
repaidAmount);
+
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
currentInstallment,
+ repaidAmount, zeroMoney, zeroMoney,
zeroMoney));
+ }
+ loanTransactionMapped = true;
+ break;
+ }
+ }
+
+ // New installment will be added (N+1 scenario)
+ if (!loanTransactionMapped) {
+ if (loanTransaction.getTransactionDate().equals(pastDueDate)) {
+ LoanRepaymentScheduleInstallment currentInstallment =
installments.get(installments.size() - 1);
+
currentInstallment.addToCredits(transactionAmount.getAmount());
+ currentInstallment.addToPrincipal(transactionDate,
transactionAmount);
+ if (repaidAmount.isGreaterThanZero()) {
+
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(),
repaidAmount);
+
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
currentInstallment,
+ repaidAmount, zeroMoney, zeroMoney,
zeroMoney));
+ }
+ } else {
+ Loan loan = loanTransaction.getLoan();
+ LoanRepaymentScheduleInstallment installment = new
LoanRepaymentScheduleInstallment(loan, (installments.size() + 1),
+ pastDueDate, transactionDate,
transactionAmount.getAmount(), zeroMoney.getAmount(), zeroMoney.getAmount(),
+ zeroMoney.getAmount(), false, null);
+ installment.markAsAdditional();
+ installment.addToCredits(transactionAmount.getAmount());
+ loan.addLoanRepaymentScheduleInstallment(installment);
+
+ if (repaidAmount.isGreaterThanZero()) {
+
installment.payPrincipalComponent(loanTransaction.getTransactionDate(),
repaidAmount);
+
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
installment,
+ repaidAmount, zeroMoney, zeroMoney,
zeroMoney));
+ }
+ }
+ }
+
+
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
+ }
+ }
+
/**
* Provides support for processing the latest transaction (which should be
latest transaction) against the loan
* schedule.
@@ -524,66 +706,7 @@ public abstract class
AbstractLoanRepaymentScheduleTransactionProcessor implemen
@Override
public void handleChargeback(LoanTransaction loanTransaction,
MonetaryCurrency currency, Money overpaidAmount,
List<LoanRepaymentScheduleInstallment> installments) {
- List<LoanTransactionToRepaymentScheduleMapping> transactionMappings =
new ArrayList<>();
- final Comparator<LoanRepaymentScheduleInstallment> byDate = new
Comparator<LoanRepaymentScheduleInstallment>() {
-
- @Override
- public int compare(LoanRepaymentScheduleInstallment ord1,
LoanRepaymentScheduleInstallment ord2) {
- return ord1.getDueDate().compareTo(ord2.getDueDate());
- }
- };
- Collections.sort(installments, byDate);
- final Money zeroMoney = Money.zero(currency);
- Money transactionAmountUnprocessed =
loanTransaction.getAmount(currency);
- if (overpaidAmount.isGreaterThanZero()) {
- transactionAmountUnprocessed =
loanTransaction.getAmount(currency).minus(overpaidAmount);
- if (transactionAmountUnprocessed.isLessThanZero()) {
- transactionAmountUnprocessed = zeroMoney;
- }
- }
-
- if (transactionAmountUnprocessed.isGreaterThanZero()) {
- final LocalDate transactionDate =
loanTransaction.getTransactionDate();
- boolean loanTransactionMapped = false;
- LocalDate pastDueDate = null;
- for (final LoanRepaymentScheduleInstallment currentInstallment :
installments) {
- pastDueDate = currentInstallment.getDueDate();
- if (!currentInstallment.isAdditional() &&
currentInstallment.getDueDate().isAfter(transactionDate)) {
-
currentInstallment.addToCredits(transactionAmountUnprocessed.getAmount());
- currentInstallment.addToPrincipal(transactionDate,
transactionAmountUnprocessed);
-
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
currentInstallment,
- transactionAmountUnprocessed, zeroMoney,
zeroMoney, zeroMoney));
-
- loanTransactionMapped = true;
-
- break;
-
- // If already exists an additional installment just update
the due date and
- // principal from the Loan charge back transaction
- } else if (currentInstallment.isAdditional()) {
- currentInstallment.updateDueChargeback(transactionDate,
transactionAmountUnprocessed);
- loanTransactionMapped = true;
- break;
- }
- }
-
- // New installment will be added (N+1 scenario)
- if (!loanTransactionMapped) {
- Loan loan = loanTransaction.getLoan();
- final Set<LoanInterestRecalcualtionAdditionalDetails>
compoundingDetails = null;
- LoanRepaymentScheduleInstallment installment = new
LoanRepaymentScheduleInstallment(loan, (installments.size() + 1),
- pastDueDate, transactionDate,
transactionAmountUnprocessed.getAmount(), zeroMoney.getAmount(),
- zeroMoney.getAmount(), zeroMoney.getAmount(), false,
compoundingDetails);
- installment.markAsAdditional();
-
installment.addToCredits(transactionAmountUnprocessed.getAmount());
- loan.addLoanRepaymentScheduleInstallment(installment);
-
-
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
installment,
- transactionAmountUnprocessed, zeroMoney, zeroMoney,
zeroMoney));
- }
-
-
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
- }
+ processCreditTransaction(loanTransaction, overpaidAmount, currency,
installments);
}
@Override
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
index afac84139..a6a358fdb 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
@@ -1302,7 +1302,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
private void validateLoanTransactionAmountChargeBack(LoanTransaction
loanTransaction, LoanTransaction chargebackTransaction) {
BigDecimal actualAmount = BigDecimal.ZERO;
for (LoanTransactionRelation loanTransactionRelation :
loanTransaction.getLoanTransactionRelations()) {
- if
(loanTransactionRelation.getRelationType().equals(LoanTransactionRelationTypeEnum.CHARGEBACK))
{
+ if
(loanTransactionRelation.getRelationType().equals(LoanTransactionRelationTypeEnum.CHARGEBACK)
+ &&
loanTransactionRelation.getToTransaction().isNotReversed()) {
actualAmount =
actualAmount.add(loanTransactionRelation.getToTransaction().getPrincipalPortion());
}
}
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java
index e462aeb67..ef48eb58a 100644
---
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java
+++
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/LoanDelinquencyDomainServiceTest.java
@@ -26,10 +26,12 @@ import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.function.Predicate;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import
org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
@@ -136,6 +138,8 @@ public class LoanDelinquencyDomainServiceTest {
when(loanProduct.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
when(loan.getLoanProduct()).thenReturn(loanProduct);
when(loan.getRepaymentScheduleInstallments()).thenReturn(repaymentScheduleInstallments);
+
when(loan.getLoanTransactions(Mockito.any(Predicate.class))).thenReturn(Collections.emptyList());
+
when(loan.getLastLoanRepaymentScheduleInstallment()).thenReturn(repaymentScheduleInstallments.get(0));
when(loan.getCurrency()).thenReturn(currency);
CollectionData collectionData =
underTest.getOverdueCollectionData(loan);
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
index c69acfd3f..79adfb007 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
@@ -7137,6 +7137,235 @@ public class ClientLoanIntegrationTest {
}
}
+ @Test
+ public void
testCreditBalanceRefundAfterMaturityWithReverseReplayOfRepayments() {
+ try {
+
GlobalConfigurationHelper.updateIsAutomaticExternalIdGenerationEnabled(this.requestSpec,
this.responseSpec, true);
+
GlobalConfigurationHelper.updateIsBusinessDateEnabled(this.requestSpec,
this.responseSpec, true);
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
+ .date("2022.10.10").dateFormat("yyyy.MM.dd").locale("en"));
+
+ final Account assetAccount =
this.accountHelper.createAssetAccount();
+ final Account incomeAccount =
this.accountHelper.createIncomeAccount();
+ final Account expenseAccount =
this.accountHelper.createExpenseAccount();
+ final Account overpaymentAccount =
this.accountHelper.createLiabilityAccount();
+
+ final Integer loanProductID =
createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount,
incomeAccount,
+ expenseAccount, overpaymentAccount);
+
+ final Integer clientID = ClientHelper.createClient(requestSpec,
responseSpec, "01 January 2011");
+
+ final Integer loanID = applyForLoanApplication(clientID,
loanProductID);
+
+ HashMap<String, Object> loanStatusHashMap =
LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID);
+ LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
+
+ loanStatusHashMap = this.loanTransactionHelper.approveLoan("02
September 2022", loanID);
+ LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
+
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
+
+ loanStatusHashMap =
this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount("03 September
2022", loanID, "1000");
+ LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+
+ this.loanTransactionHelper.makeRepayment("04 September 2022",
Float.parseFloat("100"), loanID);
+ this.loanTransactionHelper.makeRepayment("05 September 2022",
Float.parseFloat("1100"), loanID);
+
+ GetLoansLoanIdResponse loanDetails =
this.loanTransactionHelper.getLoanDetails((long) loanID);
+ assertEquals(200.0, loanDetails.getTotalOverpaid());
+ assertTrue(loanDetails.getStatus().getOverpaid());
+
+ this.loanTransactionHelper.makeCreditBalanceRefund((long) loanID,
new PostLoansLoanIdTransactionsRequest()
+ .transactionAmount(200.0).transactionDate("10 October
2022").dateFormat("dd MMMM yyyy").locale("en"));
+
+ loanDetails = this.loanTransactionHelper.getLoanDetails((long)
loanID);
+ assertTrue(loanDetails.getStatus().getClosedObligationsMet());
+
+ assertEquals(2,
loanDetails.getRepaymentSchedule().getPeriods().size());
+ assertEquals(1000,
loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue());
+
+ assertEquals(100.0,
loanDetails.getTransactions().get(1).getAmount());
+ assertEquals(100.0,
loanDetails.getTransactions().get(1).getPrincipalPortion());
+ assertEquals(LocalDate.of(2022, 9, 4),
loanDetails.getTransactions().get(1).getDate());
+ assertEquals(900.0,
loanDetails.getTransactions().get(1).getOutstandingLoanBalance());
+ assertEquals(1100.0,
loanDetails.getTransactions().get(2).getAmount());
+ assertEquals(900.0,
loanDetails.getTransactions().get(2).getPrincipalPortion());
+ assertEquals(200.0,
loanDetails.getTransactions().get(2).getOverpaymentPortion());
+ assertEquals(LocalDate.of(2022, 9, 5),
loanDetails.getTransactions().get(2).getDate());
+ assertEquals(0.0,
loanDetails.getTransactions().get(2).getOutstandingLoanBalance());
+ assertEquals(200.0,
loanDetails.getTransactions().get(3).getAmount());
+ assertEquals(200.0,
loanDetails.getTransactions().get(3).getOverpaymentPortion());
+ assertEquals(LocalDate.of(2022, 10, 10),
loanDetails.getTransactions().get(3).getDate());
+ assertEquals(0.0,
loanDetails.getTransactions().get(3).getOutstandingLoanBalance());
+
+
this.loanTransactionHelper.reverseLoanTransaction(loanDetails.getId(),
loanDetails.getTransactions().get(1).getId(),
+ new
PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat("dd MMMM
yyyy").transactionAmount(0.0)
+ .transactionDate("10 October 2022").locale("en"));
+
+ loanDetails = this.loanTransactionHelper.getLoanDetails((long)
loanID);
+
+ assertEquals(100.0,
loanDetails.getTransactions().get(1).getAmount());
+ assertEquals(100.0,
loanDetails.getTransactions().get(1).getPrincipalPortion());
+ assertEquals(LocalDate.of(2022, 9, 4),
loanDetails.getTransactions().get(1).getDate());
+
assertTrue(loanDetails.getTransactions().get(1).getManuallyReversed());
+
+ assertEquals(1100.0,
loanDetails.getTransactions().get(2).getAmount());
+ assertEquals(1000.0,
loanDetails.getTransactions().get(2).getPrincipalPortion());
+ assertEquals(100.0,
loanDetails.getTransactions().get(2).getOverpaymentPortion());
+ assertEquals(LocalDate.of(2022, 9, 5),
loanDetails.getTransactions().get(2).getDate());
+ assertEquals(0.0,
loanDetails.getTransactions().get(2).getOutstandingLoanBalance());
+ assertEquals(1,
loanDetails.getTransactions().get(2).getTransactionRelations().size());
+
+ assertEquals(200.0,
loanDetails.getTransactions().get(3).getAmount());
+ assertEquals(100.0,
loanDetails.getTransactions().get(3).getPrincipalPortion());
+ assertEquals(100.0,
loanDetails.getTransactions().get(3).getOverpaymentPortion());
+ assertEquals(100.0,
loanDetails.getTransactions().get(3).getOutstandingLoanBalance());
+ assertEquals(LocalDate.of(2022, 10, 10),
loanDetails.getTransactions().get(3).getDate());
+ assertEquals(1,
loanDetails.getTransactions().get(3).getTransactionRelations().size());
+
+ assertTrue(loanDetails.getStatus().getActive());
+
+ assertEquals(3,
loanDetails.getRepaymentSchedule().getPeriods().size());
+ assertEquals(1000,
loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue());
+
assertTrue(loanDetails.getRepaymentSchedule().getPeriods().get(1).getComplete());
+ assertEquals(200,
loanDetails.getRepaymentSchedule().getPeriods().get(2).getPrincipalDue());
+
assertFalse(loanDetails.getRepaymentSchedule().getPeriods().get(2).getComplete());
+ assertEquals(100.0,
loanDetails.getRepaymentSchedule().getPeriods().get(2).getPrincipalPaid());
+ assertEquals(100.0,
loanDetails.getRepaymentSchedule().getPeriods().get(2).getPrincipalOutstanding());
+
+ } finally {
+
GlobalConfigurationHelper.updateIsAutomaticExternalIdGenerationEnabled(this.requestSpec,
this.responseSpec, false);
+
GlobalConfigurationHelper.updateIsBusinessDateEnabled(this.requestSpec,
this.responseSpec, false);
+ }
+ }
+
+ @Test
+ public void
testCreditBalanceRefundBeforeMaturityWithReverseReplayOfRepaymentsAndRefund() {
+ try {
+
GlobalConfigurationHelper.updateIsAutomaticExternalIdGenerationEnabled(this.requestSpec,
this.responseSpec, true);
+
GlobalConfigurationHelper.updateIsBusinessDateEnabled(this.requestSpec,
this.responseSpec, true);
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
+ .date("2022.10.10").dateFormat("yyyy.MM.dd").locale("en"));
+
+ final Account assetAccount =
this.accountHelper.createAssetAccount();
+ final Account incomeAccount =
this.accountHelper.createIncomeAccount();
+ final Account expenseAccount =
this.accountHelper.createExpenseAccount();
+ final Account overpaymentAccount =
this.accountHelper.createLiabilityAccount();
+
+ final Integer loanProductID =
createLoanProductWithPeriodicAccrualAccountingNoInterest(assetAccount,
incomeAccount,
+ expenseAccount, overpaymentAccount);
+
+ final Integer clientID = ClientHelper.createClient(requestSpec,
responseSpec, "01 January 2011");
+
+ final Integer loanID = applyForLoanApplication(clientID,
loanProductID);
+
+ HashMap<String, Object> loanStatusHashMap =
LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID);
+ LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
+
+ loanStatusHashMap = this.loanTransactionHelper.approveLoan("02
September 2022", loanID);
+ LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
+
LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
+
+ loanStatusHashMap =
this.loanTransactionHelper.disburseLoanWithNetDisbursalAmount("03 September
2022", loanID, "1000");
+ LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+
+ this.loanTransactionHelper.makeRepayment("04 September 2022",
Float.parseFloat("500"), loanID);
+ this.loanTransactionHelper.makeRepayment("05 September 2022",
Float.parseFloat("700"), loanID);
+
+ GetLoansLoanIdResponse loanDetails =
this.loanTransactionHelper.getLoanDetails((long) loanID);
+ assertEquals(200.0, loanDetails.getTotalOverpaid());
+ assertTrue(loanDetails.getStatus().getOverpaid());
+
+ this.loanTransactionHelper.makeCreditBalanceRefund((long) loanID,
new PostLoansLoanIdTransactionsRequest()
+ .transactionAmount(200.0).transactionDate("06 September
2022").dateFormat("dd MMMM yyyy").locale("en"));
+
+ this.loanTransactionHelper.makeMerchantIssuedRefund((long) loanID,
new PostLoansLoanIdTransactionsRequest().locale("en")
+ .dateFormat("dd MMMM yyyy").transactionDate("07 September
2022").transactionAmount(500.0));
+
+ this.loanTransactionHelper.makeCreditBalanceRefund((long) loanID,
new PostLoansLoanIdTransactionsRequest()
+ .transactionAmount(500.0).transactionDate("08 September
2022").dateFormat("dd MMMM yyyy").locale("en"));
+
+ loanDetails = this.loanTransactionHelper.getLoanDetails((long)
loanID);
+ assertTrue(loanDetails.getStatus().getClosedObligationsMet());
+
+ assertEquals(2,
loanDetails.getRepaymentSchedule().getPeriods().size());
+ assertEquals(1000,
loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue());
+
+ assertEquals(500.0,
loanDetails.getTransactions().get(1).getAmount());
+ assertEquals(500.0,
loanDetails.getTransactions().get(1).getPrincipalPortion());
+ assertEquals(LocalDate.of(2022, 9, 4),
loanDetails.getTransactions().get(1).getDate());
+ assertEquals(500.0,
loanDetails.getTransactions().get(1).getOutstandingLoanBalance());
+
+ assertEquals(700.0,
loanDetails.getTransactions().get(2).getAmount());
+ assertEquals(500.0,
loanDetails.getTransactions().get(2).getPrincipalPortion());
+ assertEquals(200.0,
loanDetails.getTransactions().get(2).getOverpaymentPortion());
+ assertEquals(LocalDate.of(2022, 9, 5),
loanDetails.getTransactions().get(2).getDate());
+ assertEquals(0.0,
loanDetails.getTransactions().get(2).getOutstandingLoanBalance());
+
+ assertEquals(200.0,
loanDetails.getTransactions().get(3).getAmount());
+ assertEquals(200.0,
loanDetails.getTransactions().get(3).getOverpaymentPortion());
+ assertEquals(LocalDate.of(2022, 9, 6),
loanDetails.getTransactions().get(3).getDate());
+ assertEquals(0.0,
loanDetails.getTransactions().get(3).getOutstandingLoanBalance());
+
+ assertEquals(500.0,
loanDetails.getTransactions().get(4).getAmount());
+ assertEquals(500.0,
loanDetails.getTransactions().get(4).getOverpaymentPortion());
+ assertEquals(LocalDate.of(2022, 9, 7),
loanDetails.getTransactions().get(4).getDate());
+ assertEquals(0.0,
loanDetails.getTransactions().get(4).getOutstandingLoanBalance());
+
+ assertEquals(500.0,
loanDetails.getTransactions().get(5).getAmount());
+ assertEquals(500.0,
loanDetails.getTransactions().get(5).getOverpaymentPortion());
+ assertEquals(LocalDate.of(2022, 9, 8),
loanDetails.getTransactions().get(5).getDate());
+ assertEquals(0.0,
loanDetails.getTransactions().get(5).getOutstandingLoanBalance());
+
+
this.loanTransactionHelper.reverseLoanTransaction(loanDetails.getId(),
loanDetails.getTransactions().get(2).getId(),
+ new
PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat("dd MMMM
yyyy").transactionAmount(0.0)
+ .transactionDate("07 September
2022").locale("en"));
+
+ loanDetails = this.loanTransactionHelper.getLoanDetails((long)
loanID);
+
+ assertEquals(500.0,
loanDetails.getTransactions().get(1).getAmount());
+ assertEquals(500.0,
loanDetails.getTransactions().get(1).getPrincipalPortion());
+ assertEquals(LocalDate.of(2022, 9, 4),
loanDetails.getTransactions().get(1).getDate());
+ assertEquals(500.0,
loanDetails.getTransactions().get(1).getOutstandingLoanBalance());
+
+ assertEquals(700.0,
loanDetails.getTransactions().get(2).getAmount());
+ assertEquals(500.0,
loanDetails.getTransactions().get(2).getPrincipalPortion());
+ assertEquals(200.0,
loanDetails.getTransactions().get(2).getOverpaymentPortion());
+ assertEquals(LocalDate.of(2022, 9, 5),
loanDetails.getTransactions().get(2).getDate());
+ assertEquals(0.0,
loanDetails.getTransactions().get(2).getOutstandingLoanBalance());
+
assertTrue(loanDetails.getTransactions().get(2).getManuallyReversed());
+
+ assertEquals(200.0,
loanDetails.getTransactions().get(3).getAmount());
+ assertEquals(200.0,
loanDetails.getTransactions().get(3).getPrincipalPortion());
+ assertEquals(LocalDate.of(2022, 9, 6),
loanDetails.getTransactions().get(3).getDate());
+ assertEquals(700.0,
loanDetails.getTransactions().get(3).getOutstandingLoanBalance());
+ assertEquals(1,
loanDetails.getTransactions().get(3).getTransactionRelations().size());
+
+ assertEquals(500.0,
loanDetails.getTransactions().get(4).getAmount());
+ assertEquals(500.0,
loanDetails.getTransactions().get(4).getPrincipalPortion());
+ assertEquals(LocalDate.of(2022, 9, 7),
loanDetails.getTransactions().get(4).getDate());
+ assertEquals(200.0,
loanDetails.getTransactions().get(4).getOutstandingLoanBalance());
+ assertEquals(1,
loanDetails.getTransactions().get(4).getTransactionRelations().size());
+
+ assertEquals(500.0,
loanDetails.getTransactions().get(5).getAmount());
+ assertEquals(500.0,
loanDetails.getTransactions().get(5).getPrincipalPortion());
+ assertEquals(LocalDate.of(2022, 9, 8),
loanDetails.getTransactions().get(5).getDate());
+ assertEquals(700.0,
loanDetails.getTransactions().get(5).getOutstandingLoanBalance());
+ assertEquals(1,
loanDetails.getTransactions().get(5).getTransactionRelations().size());
+
+ assertTrue(loanDetails.getStatus().getActive());
+
+ assertEquals(2,
loanDetails.getRepaymentSchedule().getPeriods().size());
+ assertEquals(1700,
loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue());
+
assertFalse(loanDetails.getRepaymentSchedule().getPeriods().get(1).getComplete());
+ assertEquals(1000.0,
loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid());
+ assertEquals(700.0,
loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding());
+
+ } finally {
+
GlobalConfigurationHelper.updateIsAutomaticExternalIdGenerationEnabled(this.requestSpec,
this.responseSpec, false);
+
GlobalConfigurationHelper.updateIsBusinessDateEnabled(this.requestSpec,
this.responseSpec, false);
+ }
+ }
+
private Integer applyForLoanApplication(final Integer clientID, final
Integer loanProductID) {
LOG.info("--------------------------------APPLYING FOR LOAN
APPLICATION--------------------------------");
final String loanApplicationJSON = new
LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1")