FINERACT-2107: Interest Refund - Transaction Amount Recalculation

This commit is contained in:
Soma Sörös
2024-11-14 09:59:08 +01:00
committed by Adam Saghy
parent 4e8fc2c0a8
commit 6286d1f821
14 changed files with 276 additions and 75 deletions
@@ -3969,7 +3969,7 @@ Feature: LoanRepayment
| 15 January 2024 | Merchant Issued Refund | 50.0 | 48.9 | 1.1 | 0.0 | 0.0 | 126.1 | false | | 15 January 2024 | Merchant Issued Refund | 50.0 | 48.9 | 1.1 | 0.0 | 0.0 | 126.1 | false |
| 15 January 2024 | Interest Refund | 0.29 | 0.29 | 0.0 | 0.0 | 0.0 | 125.81 | false | | 15 January 2024 | Interest Refund | 0.29 | 0.29 | 0.0 | 0.0 | 0.0 | 125.81 | false |
| 16 January 2024 | Payout Refund | 50.0 | 49.95 | 0.05 | 0.0 | 0.0 | 75.86 | false | | 16 January 2024 | Payout Refund | 50.0 | 49.95 | 0.05 | 0.0 | 0.0 | 75.86 | false |
| 16 January 2024 | Interest Refund | 0.31 | 0.31 | 0.0 | 0.0 | 0.0 | 75.55 | false | | 16 January 2024 | Interest Refund | 0.3 | 0.3 | 0.0 | 0.0 | 0.0 | 75.56 | false |
Then In Loan Transactions the "4"th Transaction has relationship type=RELATED with the "3"th Transaction Then In Loan Transactions the "4"th Transaction has relationship type=RELATED with the "3"th Transaction
Then In Loan Transactions the "6"th Transaction has relationship type=RELATED with the "5"th Transaction Then In Loan Transactions the "6"th Transaction has relationship type=RELATED with the "5"th Transaction
When Customer undo "1"th "Merchant Issued Refund" transaction made on "15 January 2024" When Customer undo "1"th "Merchant Issued Refund" transaction made on "15 January 2024"
@@ -141,6 +141,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCom
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod; import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
@@ -5452,6 +5453,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
} }
} }
public List<LoanTransactionType> getSupportedInterestRefundTransactionTypes() {
return getLoanProductRelatedDetail().getSupportedInterestRefundTypes().stream()
.map(LoanSupportedInterestRefundTypes::getTransactionType).toList();
}
public LoanTransaction getLastUserTransaction() { public LoanTransaction getLastUserTransaction() {
return getLoanTransactions().stream() // return getLoanTransactions().stream() //
.filter(LoanTransaction::isNotReversed) // .filter(LoanTransaction::isNotReversed) //
@@ -1052,6 +1052,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
return getTypeOf().isInterestRefund(); return getTypeOf().isInterestRefund();
} }
public void updateAmount(BigDecimal bigDecimal) {
this.amount = bigDecimal;
}
// TODO missing hashCode(), equals(Object obj), but probably OK as long as // TODO missing hashCode(), equals(Object obj), but probably OK as long as
// this is never stored in a Collection. // this is never stored in a Collection.
} }
@@ -18,15 +18,23 @@
*/ */
package org.apache.fineract.portfolio.loanaccount.service; package org.apache.fineract.portfolio.loanaccount.service;
import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
public interface InterestRefundService { public interface InterestRefundService {
boolean canHandle(Loan loan); boolean canHandle(Loan loan);
BigDecimal calculateInterestRefundAmount(Long loanId, BigDecimal relatedRefundTransactionAmount, @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
LocalDate relatedRefundTransactionDate); Money totalInterestByTransactions(LoanRepaymentScheduleTransactionProcessor processor, Long loanId,
LocalDate relatedRefundTransactionDate, List<LoanTransaction> newTransactions, List<Long> oldTransactionIds);
Money getTotalInterestRefunded(List<LoanTransaction> loanTransactions, MonetaryCurrency currency);
} }
@@ -57,6 +57,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.NotImplementedException; import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
@@ -85,6 +86,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.Tra
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.domain.AllocationType; import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
@@ -102,6 +104,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY_NAME = "Advanced payment allocation strategy"; public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY_NAME = "Advanced payment allocation strategy";
public final EMICalculator emiCalculator; public final EMICalculator emiCalculator;
public final InterestRefundService interestRefundService;
@Override @Override
public String getCode() { public String getCode() {
@@ -196,6 +199,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
LoanTransaction transaction = changeOperation.getLoanTransaction().get(); LoanTransaction transaction = changeOperation.getLoanTransaction().get();
processSingleTransaction(transaction, ctx); processSingleTransaction(transaction, ctx);
transaction = getProcessedTransaction(changedTransactionDetail, transaction); transaction = getProcessedTransaction(changedTransactionDetail, transaction);
ctx.getAlreadyProcessedTransactions().add(transaction);
if (transaction.isOverPaid() && transaction.isRepaymentLikeType()) { // TODO CREDIT, DEBIT if (transaction.isOverPaid() && transaction.isRepaymentLikeType()) { // TODO CREDIT, DEBIT
overpaidTransactions.add(transaction); overpaidTransactions.add(transaction);
} }
@@ -268,9 +272,10 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
case CHARGEBACK -> handleChargeback(loanTransaction, ctx); case CHARGEBACK -> handleChargeback(loanTransaction, ctx);
case CREDIT_BALANCE_REFUND -> case CREDIT_BALANCE_REFUND ->
handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder()); handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
case INTEREST_REFUND, REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
DOWN_PAYMENT, WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER -> WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER ->
handleRepayment(loanTransaction, ctx); handleRepayment(loanTransaction, ctx);
case INTEREST_REFUND -> handleInterestRefund(loanTransaction, ctx);
case CHARGE_OFF -> handleChargeOff(loanTransaction, ctx); case CHARGE_OFF -> handleChargeOff(loanTransaction, ctx);
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, ctx); case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, ctx);
case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed."); case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed.");
@@ -284,6 +289,25 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
} }
} }
private void handleInterestRefund(LoanTransaction loanTransaction, TransactionCtx ctx) {
if (ctx instanceof ProgressiveTransactionCtx progCtx) {
Money interestBeforeRefund = emiCalculator.getSumOfDueInterestsOnDate(progCtx.getModel(), loanTransaction.getDateOf());
List<Long> unmodifiedTransactionIds = progCtx.getAlreadyProcessedTransactions().stream().filter(LoanTransaction::isNotReversed)
.map(AbstractPersistableCustom::getId).toList();
List<LoanTransaction> modifiedTransactions = new ArrayList<>(progCtx.getAlreadyProcessedTransactions().stream()
.filter(LoanTransaction::isNotReversed).filter(tr -> tr.getId() == null).toList());
if (!modifiedTransactions.isEmpty()) {
Money interestAfterRefund = interestRefundService.totalInterestByTransactions(this, loanTransaction.getLoan().getId(),
loanTransaction.getDateOf(), modifiedTransactions, unmodifiedTransactionIds);
Money newAmount = interestBeforeRefund.minus(progCtx.getSumOfInterestRefundAmount()).minus(interestAfterRefund);
loanTransaction.updateAmount(newAmount.getAmount());
}
progCtx.setSumOfInterestRefundAmount(progCtx.getSumOfInterestRefundAmount().add(loanTransaction.getAmount()));
}
handleRepayment(loanTransaction, ctx);
}
private void handleReAmortization(LoanTransaction loanTransaction, TransactionCtx transactionCtx) { private void handleReAmortization(LoanTransaction loanTransaction, TransactionCtx transactionCtx) {
LocalDate transactionDate = loanTransaction.getTransactionDate(); LocalDate transactionDate = loanTransaction.getTransactionDate();
List<LoanRepaymentScheduleInstallment> previousInstallments = transactionCtx.getInstallments().stream() // List<LoanRepaymentScheduleInstallment> previousInstallments = transactionCtx.getInstallments().stream() //
@@ -19,14 +19,17 @@
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl; package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
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.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
@@ -37,11 +40,15 @@ public class ProgressiveTransactionCtx extends TransactionCtx {
private final ProgressiveLoanInterestScheduleModel model; private final ProgressiveLoanInterestScheduleModel model;
@Setter @Setter
private LocalDate lastOverdueBalanceChange = null; private LocalDate lastOverdueBalanceChange = null;
private List<LoanTransaction> alreadyProcessedTransactions = new ArrayList<>();
@Setter
private Money sumOfInterestRefundAmount;
public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail, Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
ProgressiveLoanInterestScheduleModel model) { ProgressiveLoanInterestScheduleModel model) {
super(currency, installments, charges, overpaymentHolder, changedTransactionDetail); super(currency, installments, charges, overpaymentHolder, changedTransactionDetail);
sumOfInterestRefundAmount = model.getZero();
this.model = model; this.model = model;
} }
} }
@@ -62,4 +62,6 @@ public interface EMICalculator {
LocalDate targetDate); LocalDate targetDate);
OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel model, LocalDate targetDate); OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel model, LocalDate targetDate);
Money getSumOfDueInterestsOnDate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate subjectDate);
} }
@@ -716,4 +716,22 @@ public final class ProgressiveEMICalculator implements EMICalculator {
return new ProgressiveLoanInterestScheduleModel(repaymentModels, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); return new ProgressiveLoanInterestScheduleModel(repaymentModels, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc);
} }
/**
* Calculates the sum of due interests on interest periods.
*
* @param scheduleModel
* schedule model
* @param subjectDate
* the date to calculate the interest for.
* @return sum of due interests
*/
@Override
public Money getSumOfDueInterestsOnDate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate subjectDate) {
return scheduleModel.repaymentPeriods().stream().map(RepaymentPeriod::getDueDate) //
.map(repaymentPeriodDueDate -> getDueAmounts(scheduleModel, repaymentPeriodDueDate, subjectDate) //
.getDueInterest()) //
.reduce(scheduleModel.getZero(), Money::add); //
}
} }
@@ -109,7 +109,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator); underTest = new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, null);
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
@@ -34,6 +34,7 @@ import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDoma
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException; import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
@@ -143,8 +144,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper; private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
private final DelinquencyReadPlatformService delinquencyReadPlatformService; private final DelinquencyReadPlatformService delinquencyReadPlatformService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService; private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final InterestRefundServiceDelegate interestRefundServiceDelegate;
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
private final InterestRefundServiceDelegate interestRefundServiceDelegate;
@Transactional @Transactional
@Override @Override
@@ -165,17 +166,26 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
this.loanCollateralManagementRepository.saveAll(loanCollateralManagementSet); this.loanCollateralManagementRepository.saveAll(loanCollateralManagementSet);
} }
private LoanTransaction createInterestRefundLoanTransaction(Loan loan, final LocalDate transactionDate, private LoanTransaction createInterestRefundLoanTransaction(Loan loan, LoanTransaction refundTransaction) {
BigDecimal relatedRefundTransactionAmount) {
InterestRefundService interestRefundService = interestRefundServiceDelegate.lookupInterestRefundService(loan); InterestRefundService interestRefundService = interestRefundServiceDelegate.lookupInterestRefundService(loan);
if (interestRefundService == null) { if (interestRefundService == null) {
return null; return null;
} }
BigDecimal interestRefundAmount = interestRefundService.calculateInterestRefundAmount(loan.getId(), relatedRefundTransactionAmount,
transactionDate); Money totalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(), refundTransaction.getTransactionDate(),
List.of(), loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList());
Money previouslyRefundedInterests = interestRefundService.getTotalInterestRefunded(loan.getLoanTransactions(), loan.getCurrency());
Money newTotalInterest = interestRefundService.totalInterestByTransactions(null, loan.getId(),
refundTransaction.getTransactionDate(), List.of(refundTransaction),
loan.getLoanTransactions().stream().map(AbstractPersistableCustom::getId).toList());
BigDecimal interestRefundAmount = totalInterest.minus(previouslyRefundedInterests).minus(newTotalInterest).getAmount();
final ExternalId txnExternalId = externalIdFactory.create(); final ExternalId txnExternalId = externalIdFactory.create();
businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionInterestRefundPreBusinessEvent(loan)); businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionInterestRefundPreBusinessEvent(loan));
return LoanTransaction.interestRefund(loan, interestRefundAmount, transactionDate, txnExternalId); return LoanTransaction.interestRefund(loan, interestRefundAmount, refundTransaction.getDateOf(), txnExternalId);
} }
@Transactional @Transactional
@@ -867,7 +877,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
LoanTransaction interestRefundTransaction = null; LoanTransaction interestRefundTransaction = null;
if (shouldCreateInterestRefundTransaction) { if (shouldCreateInterestRefundTransaction) {
interestRefundTransaction = createInterestRefundLoanTransaction(loan, transactionDate, transactionAmount); interestRefundTransaction = createInterestRefundLoanTransaction(loan, refundTransaction);
if (interestRefundTransaction != null) { if (interestRefundTransaction != null) {
interestRefundTransaction.getLoanTransactionRelations().add(LoanTransactionRelation interestRefundTransaction.getLoanTransactionRelations().add(LoanTransactionRelation
.linkToTransaction(interestRefundTransaction, refundTransaction, LoanTransactionRelationTypeEnum.RELATED)); .linkToTransaction(interestRefundTransaction, refundTransaction, LoanTransactionRelationTypeEnum.RELATED));
@@ -21,12 +21,14 @@ package org.apache.fineract.portfolio.loanaccount.service;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class InterestRefundServiceDelegate { public class InterestRefundServiceDelegate {
@Lazy
private final List<InterestRefundService> interestRefundService; private final List<InterestRefundService> interestRefundService;
public InterestRefundService lookupInterestRefundService(final Loan loan) { public InterestRefundService lookupInterestRefundService(final Loan loan) {
@@ -25,19 +25,22 @@ import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
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.LoanRepaymentScheduleTransactionProcessor;
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.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
import org.apache.fineract.portfolio.loanaccount.starter.AdvancedPaymentScheduleTransactionProcessorCondition; import org.apache.fineract.portfolio.loanaccount.starter.AdvancedPaymentScheduleTransactionProcessorCondition;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Propagation;
@@ -49,18 +52,13 @@ import org.springframework.transaction.annotation.Transactional;
@Service @Service
public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundService { public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundService {
private final AdvancedPaymentScheduleTransactionProcessor processor;
private final EMICalculator emiCalculator; private final EMICalculator emiCalculator;
private final LoanAssembler loanAssembler; private final LoanAssembler loanAssembler;
@Override
public boolean canHandle(Loan loan) {
return loan != null && loan.isInterestBearing() && processor.accept(loan.getTransactionProcessingStrategyCode());
}
private static void simulateRepaymentForDisbursements(LoanTransaction lt, final AtomicReference<BigDecimal> refundFinal, private static void simulateRepaymentForDisbursements(LoanTransaction lt, final AtomicReference<BigDecimal> refundFinal,
List<LoanTransaction> collect) { List<LoanTransaction> collect) {
collect.add(lt); collect.add(new LoanTransaction(lt.getLoan(), lt.getLoan().getOffice(), lt.getTypeOf().getValue(), lt.getDateOf(), lt.getAmount(),
BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null, null));
if (lt.getTypeOf().isDisbursement() && refundFinal.get().compareTo(BigDecimal.ZERO) > 0) { if (lt.getTypeOf().isDisbursement() && refundFinal.get().compareTo(BigDecimal.ZERO) > 0) {
if (lt.getAmount().compareTo(refundFinal.get()) <= 0) { if (lt.getAmount().compareTo(refundFinal.get()) <= 0) {
collect.add( collect.add(
@@ -76,52 +74,70 @@ public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundS
} }
} }
private BigDecimal totalInterest(final Loan loan, BigDecimal refundAmount, LocalDate relatedRefundTransactionDate) { private Money recalculateTotalInterest(AdvancedPaymentScheduleTransactionProcessor processor, Loan loan,
final AtomicReference<BigDecimal> refundFinal = new AtomicReference<>(refundAmount); LocalDate relatedRefundTransactionDate, List<LoanTransaction> transactionsToReprocess) {
List<LoanRepaymentScheduleInstallment> installmentsToReprocess = new ArrayList<>(
loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList());
BigDecimal payableInterest = BigDecimal.ZERO; Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessResult = processor
if (loan.getLoanTransactions().stream().anyMatch(LoanTransaction::isDisbursement)) { .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess,
List<LoanTransaction> transactionsToReprocess = new ArrayList<>(); loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges());
List<LoanTransactionType> interestRefundTypes = loan.getLoanProductRelatedDetail().getSupportedInterestRefundTypes().stream() loan.getLoanTransactions().addAll(reprocessResult.getLeft().getCurrentTransactionToOldId().keySet());
.map(LoanSupportedInterestRefundTypes::getTransactionType).toList(); ProgressiveLoanInterestScheduleModel modelAfter = reprocessResult.getRight();
// add already interest refunded amounts to refund amount
// it is necessary to avoid multi disbursed refund
loan.getLoanTransactions().stream() //
.filter(lt -> !lt.isReversed()) //
.filter(lt -> interestRefundTypes.contains(lt.getTypeOf())) //
.forEach(t -> refundFinal.set(refundFinal.get().add(t.getAmount()))); //
loan.getLoanTransactions().stream() //
.filter(lt -> !lt.isReversed()) //
.filter(lt -> !lt.isAccrual() && !lt.isAccrualActivity() && !lt.isInterestRefund()) //
.filter(loanTransaction -> !interestRefundTypes.contains(loanTransaction.getTypeOf())) //
.forEach(lt -> simulateRepaymentForDisbursements(lt, refundFinal, transactionsToReprocess)); //
List<LoanRepaymentScheduleInstallment> installmentsToReprocess = new ArrayList<>( return emiCalculator.getSumOfDueInterestsOnDate(modelAfter, relatedRefundTransactionDate);
loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList()); }
Pair<ChangedTransactionDetail, ProgressiveLoanInterestScheduleModel> reprocessResult = processor @Override
.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess, public boolean canHandle(Loan loan) {
loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges()); String s = loan.getTransactionProcessingStrategyCode();
loan.getLoanTransactions().addAll(reprocessResult.getLeft().getCurrentTransactionToOldId().keySet()); return AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY_NAME.equalsIgnoreCase(s)
ProgressiveLoanInterestScheduleModel modelAfter = reprocessResult.getRight(); || AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equalsIgnoreCase(s);
}
payableInterest = installmentsToReprocess.stream() // private boolean isTransactionNeededForInterestRefundCalculations(LoanTransaction lt) {
.map(installment -> emiCalculator // return lt.isNotReversed() && !lt.isAccrual() && !lt.isAccrualActivity() && !lt.isInterestRefund();
.getDueAmounts(modelAfter, installment.getDueDate(), relatedRefundTransactionDate) //
.getDueInterest() //
.getAmount()) //
.reduce(BigDecimal.ZERO, BigDecimal::add); //
}
return payableInterest;
} }
@Override @Override
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW) @Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
public BigDecimal calculateInterestRefundAmount(Long loanId, BigDecimal relatedRefundTransactionAmount, public Money totalInterestByTransactions(LoanRepaymentScheduleTransactionProcessor processor, final Long loanId,
LocalDate relatedRefundTransactionDate) { LocalDate relatedRefundTransactionDate, List<LoanTransaction> newTransactions, List<Long> oldTransactionIds) {
Loan loan = loanAssembler.assembleFrom(loanId); Loan loan = loanAssembler.assembleFrom(loanId);
BigDecimal totalInterestBeforeRefund = totalInterest(loan, BigDecimal.ZERO, relatedRefundTransactionDate); if (processor == null) {
BigDecimal totalInterestAfterRefund = totalInterest(loan, relatedRefundTransactionAmount, relatedRefundTransactionDate); processor = loan.getTransactionProcessor();
return totalInterestBeforeRefund.subtract(totalInterestAfterRefund); }
if (!(processor instanceof AdvancedPaymentScheduleTransactionProcessor)) {
throw new IllegalArgumentException(
"Wrong processor implementation. ProgressiveLoanInterestRefundServiceImpl requires AdvancedPaymentScheduleTransactionProcessor");
}
List<LoanTransaction> transactionsToReprocess = new ArrayList<>();
List<LoanTransactionType> interestRefundTypes = loan.getSupportedInterestRefundTransactionTypes();
List<LoanTransaction> transactions = Stream.concat(loan.getLoanTransactions().stream() //
.filter(lt -> isTransactionNeededForInterestRefundCalculations(lt) //
&& oldTransactionIds.contains(lt.getId())), //
newTransactions.stream() //
.filter(this::isTransactionNeededForInterestRefundCalculations) //
.map(LoanTransaction::copyTransactionProperties)) //
.toList();
final AtomicReference<BigDecimal> refundFinal = new AtomicReference<>(
transactions.stream().filter(lt -> interestRefundTypes.contains(lt.getTypeOf())) //
.map(LoanTransaction::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
transactions.stream().filter(loanTransaction -> !interestRefundTypes.contains(loanTransaction.getTypeOf())) //
.forEach(lt -> simulateRepaymentForDisbursements(lt, refundFinal, transactionsToReprocess)); //
return recalculateTotalInterest((AdvancedPaymentScheduleTransactionProcessor) processor, loan, relatedRefundTransactionDate,
transactionsToReprocess);
} }
@Override
public Money getTotalInterestRefunded(List<LoanTransaction> loanTransactions, MonetaryCurrency currency) {
return Money.of(currency, loanTransactions.stream().filter(LoanTransaction::isNotReversed).filter(LoanTransaction::isInterestRefund)
.map(LoanTransaction::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
}
} }
@@ -31,11 +31,13 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.PrincipalInterestPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.RBILoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanInterestRefundServiceImpl;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
@Configuration @Configuration
public class LoanAccountAutoStarter { public class LoanAccountAutoStarter {
@@ -104,8 +106,9 @@ public class LoanAccountAutoStarter {
@Bean @Bean
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class) @Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator) { public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(EMICalculator emiCalculator,
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator); @Lazy ProgressiveLoanInterestRefundServiceImpl progressiveLoanInterestRefundService) {
return new AdvancedPaymentScheduleTransactionProcessor(emiCalculator, progressiveLoanInterestRefundService);
} }
} }
@@ -309,7 +309,7 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
logLoanTransactions(loanId); logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"),
transaction(1000.0, "Payout Refund", "09 February 2021"), transaction(87.89, "Repayment", "01 February 2021"), transaction(87.89, "Repayment", "01 February 2021"), transaction(1000.0, "Payout Refund", "09 February 2021"),
transaction(10.49, "Interest Refund", "09 February 2021")); transaction(10.49, "Interest Refund", "09 February 2021"));
}); });
} }
@@ -680,12 +680,12 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
runAt("22 January 2021", () -> { runAt("22 January 2021", () -> {
Long loanId = loanIdRef.get(); Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("PayoutRefund", "22 January 2021", 500F, loanId.intValue()); loanTransactionHelper.makeLoanRepayment("PayoutRefund", "22 January 2021", 500F, loanId.intValue());
logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), // verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(500.0, "Merchant Issued Refund", "14 January 2021"), // transaction(500.0, "Merchant Issued Refund", "14 January 2021"), //
transaction(1.78, "Interest Refund", "14 January 2021"), // transaction(1.78, "Interest Refund", "14 January 2021"), //
transaction(500.0, "Payout Refund", "22 January 2021"), // transaction(500.0, "Payout Refund", "22 January 2021"), //
transaction(2.87, "Interest Refund", "22 January 2021") // transaction(2.88, "Interest Refund", "22 January 2021") //
); );
}); });
} }
@@ -830,6 +830,7 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
runAt("26 January 2021", () -> { runAt("26 January 2021", () -> {
Long loanId = loanIdRef.get(); Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("PayoutRefund", "26 January 2021", 400F, loanId.intValue()); loanTransactionHelper.makeLoanRepayment("PayoutRefund", "26 January 2021", 400F, loanId.intValue());
logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(200.0, "Disbursement", "01 January 2021"), // verifyTransactions(loanId, transaction(200.0, "Disbursement", "01 January 2021"), //
transaction(300.0, "Disbursement", "01 January 2021"), // transaction(300.0, "Disbursement", "01 January 2021"), //
@@ -879,11 +880,11 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
}); });
runAt("1 February 2021", () -> { runAt("1 February 2021", () -> {
Long loanId = loanIdRef.get(); Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("Repayment", "1 February 2021", 171.50F, loanId.intValue()); loanTransactionHelper.makeLoanRepayment("Repayment", "1 February 2021", 171.41F, loanId.intValue());
verifyTransactions(loanId, transaction(500.0, "Disbursement", "01 January 2021"), // verifyTransactions(loanId, transaction(500.0, "Disbursement", "01 January 2021"), //
transaction(500.0, "Disbursement", "05 January 2021"), // transaction(500.0, "Disbursement", "05 January 2021"), //
transaction(171.5, "Repayment", "01 February 2021")); transaction(171.41, "Repayment", "01 February 2021"));
}); });
runAt("13 February 2021", () -> { runAt("13 February 2021", () -> {
Long loanId = loanIdRef.get(); Long loanId = loanIdRef.get();
@@ -891,7 +892,7 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
verifyTransactions(loanId, transaction(500.0, "Disbursement", "01 January 2021"), // verifyTransactions(loanId, transaction(500.0, "Disbursement", "01 January 2021"), //
transaction(500.0, "Disbursement", "05 January 2021"), // transaction(500.0, "Disbursement", "05 January 2021"), //
transaction(171.5, "Repayment", "01 February 2021"), // transaction(171.41, "Repayment", "01 February 2021"), //
transaction(250.0, "Payout Refund", "13 February 2021"), // transaction(250.0, "Payout Refund", "13 February 2021"), //
transaction(2.96, "Interest Refund", "13 February 2021") // transaction(2.96, "Interest Refund", "13 February 2021") //
); );
@@ -899,20 +900,21 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
runAt("24 February 2021", () -> { runAt("24 February 2021", () -> {
Long loanId = loanIdRef.get(); Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("MerchantIssuedRefund", "24 February 2021", 400F, loanId.intValue()); loanTransactionHelper.makeLoanRepayment("MerchantIssuedRefund", "24 February 2021", 400F, loanId.intValue());
logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(500.0, "Disbursement", "01 January 2021"), // verifyTransactions(loanId, transaction(500.0, "Disbursement", "01 January 2021"), //
transaction(500.0, "Disbursement", "05 January 2021"), // transaction(500.0, "Disbursement", "05 January 2021"), //
transaction(171.5, "Repayment", "01 February 2021"), // transaction(171.41, "Repayment", "01 February 2021"), //
transaction(250.0, "Payout Refund", "13 February 2021"), // transaction(250.0, "Payout Refund", "13 February 2021"), //
transaction(2.96, "Interest Refund", "13 February 2021"), // transaction(2.96, "Interest Refund", "13 February 2021"), //
transaction(400.0, "Merchant Issued Refund", "24 February 2021"), // transaction(400.0, "Merchant Issued Refund", "24 February 2021"), //
transaction(5.76, "Interest Refund", "24 February 2021") // transaction(5.77, "Interest Refund", "24 February 2021") //
); );
}); });
runAt("1 April 2021", () -> { runAt("1 April 2021", () -> {
Long loanId = loanIdRef.get(); Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("Repayment", "1 March 2021", 171.41F, loanId.intValue()); loanTransactionHelper.makeLoanRepayment("Repayment", "1 March 2021", 171.41F, loanId.intValue());
loanTransactionHelper.makeLoanRepayment("Repayment", "1 April 2021", 11.17F, loanId.intValue()); loanTransactionHelper.makeLoanRepayment("Repayment", "1 April 2021", 11.25F, loanId.intValue());
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertNotNull(loanDetails); Assertions.assertNotNull(loanDetails);
@@ -973,6 +975,7 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
runAt("6 April 2021", () -> { runAt("6 April 2021", () -> {
Long loanId = loanIdRef.get(); Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("MerchantIssuedRefund", "6 April 2021", 400F, loanId.intValue()); loanTransactionHelper.makeLoanRepayment("MerchantIssuedRefund", "6 April 2021", 400F, loanId.intValue());
logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(500.0, "Disbursement", "01 January 2021"), // verifyTransactions(loanId, transaction(500.0, "Disbursement", "01 January 2021"), //
transaction(500.0, "Disbursement", "05 January 2021"), // transaction(500.0, "Disbursement", "05 January 2021"), //
@@ -982,14 +985,14 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
transaction(250.0, "Payout Refund", "13 February 2021"), // transaction(250.0, "Payout Refund", "13 February 2021"), //
transaction(2.96, "Interest Refund", "13 February 2021"), // transaction(2.96, "Interest Refund", "13 February 2021"), //
transaction(400.0, "Merchant Issued Refund", "06 April 2021"), // transaction(400.0, "Merchant Issued Refund", "06 April 2021"), //
transaction(10.1, "Interest Refund", "06 April 2021") // transaction(10.11, "Interest Refund", "06 April 2021") //
); );
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertNotNull(loanDetails); Assertions.assertNotNull(loanDetails);
Assertions.assertNotNull(loanDetails.getStatus()); Assertions.assertNotNull(loanDetails.getStatus());
Assertions.assertEquals(700, loanDetails.getStatus().getId()); Assertions.assertEquals(700, loanDetails.getStatus().getId());
Assertions.assertEquals(160.15D, loanDetails.getTotalOverpaid()); Assertions.assertEquals(160.16D, loanDetails.getTotalOverpaid());
}); });
} }
@@ -1042,6 +1045,7 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
runAt("8 February 2021", () -> { runAt("8 February 2021", () -> {
Long loanId = loanIdRef.get(); Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("PayoutRefund", "8 February 2021", 250F, loanId.intValue()); loanTransactionHelper.makeLoanRepayment("PayoutRefund", "8 February 2021", 250F, loanId.intValue());
logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), // verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(400.0, "Payout Refund", "12 January 2021"), // transaction(400.0, "Payout Refund", "12 January 2021"), //
@@ -1050,12 +1054,12 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
transaction(0.66, "Interest Refund", "17 January 2021"), // transaction(0.66, "Interest Refund", "17 January 2021"), //
transaction(171.5, "Repayment", "01 February 2021"), // transaction(171.5, "Repayment", "01 February 2021"), //
transaction(250.0, "Payout Refund", "08 February 2021"), // transaction(250.0, "Payout Refund", "08 February 2021"), //
transaction(2.60, "Interest Refund", "08 February 2021") // transaction(2.61, "Interest Refund", "08 February 2021") //
); );
}); });
runAt("1 March 2021", () -> { runAt("1 March 2021", () -> {
Long loanId = loanIdRef.get(); Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("Repayment", "1 March 2021", 30.44F, loanId.intValue()); loanTransactionHelper.makeLoanRepayment("Repayment", "1 March 2021", 30.43F, loanId.intValue());
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Assertions.assertNotNull(loanDetails); Assertions.assertNotNull(loanDetails);
Assertions.assertNotNull(loanDetails.getStatus()); Assertions.assertNotNull(loanDetails.getStatus());
@@ -1063,6 +1067,103 @@ public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
}); });
} }
@Test
public void verifyUC18S1() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
runAt("1 January 2021", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
.daysInYearType(DaysInYearType.ACTUAL) //
.multiDisburseLoan(true)//
.disallowExpectedDisbursements(true)//
.maxTrancheCount(2)//
.addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.PAYOUT_REFUND) //
.addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.MERCHANT_ISSUED_REFUND) //
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
);
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.9,
12, null);
Assertions.assertNotNull(loanId);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021");
});
runAt("22 January 2021", () -> {
Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("MerchantIssuedRefund", "22 January 2021", 1000F, loanId.intValue());
logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), //
transaction(5.70, "Interest Refund", "22 January 2021"), //
transaction(5.70, "Accrual", "22 January 2021") //
);
loanTransactionHelper.makeLoanRepayment("Repayment", "10 January 2021", 85.63F, loanId.intValue());
logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(85.63, "Repayment", "10 January 2021"), //
transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), //
transaction(5.42, "Interest Refund", "22 January 2021") //
);
});
}
@Test
public void verifyUC18S2() {
AtomicReference<Long> loanIdRef = new AtomicReference<>();
AtomicReference<Long> repaymentIdRef = new AtomicReference<>();
runAt("1 January 2021", () -> {
PostLoanProductsResponse loanProduct = loanProductHelper
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
.daysInYearType(DaysInYearType.ACTUAL) //
.multiDisburseLoan(true)//
.disallowExpectedDisbursements(true)//
.maxTrancheCount(2).addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.PAYOUT_REFUND) //
.addSupportedInterestRefundTypesItem(SupportedInterestRefundTypesItem.MERCHANT_ISSUED_REFUND) //
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
);
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.9,
12, null);
Assertions.assertNotNull(loanId);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021");
});
runAt("10 January 2021", () -> {
Long loanId = loanIdRef.get();
Long response = loanTransactionHelper.makeLoanRepayment("Repayment", "10 January 2021", 85.63F, loanId.intValue())
.getResourceId();
Assertions.assertNotNull(response);
repaymentIdRef.set(response);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(85.63, "Repayment", "10 January 2021") //
);
});
runAt("22 January 2021", () -> {
Long loanId = loanIdRef.get();
loanTransactionHelper.makeLoanRepayment("MerchantIssuedRefund", "22 January 2021", 1000F, loanId.intValue());
logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
transaction(85.63, "Repayment", "10 January 2021"), //
transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), //
transaction(5.42, "Interest Refund", "22 January 2021") //
);
Long repaymentId = repaymentIdRef.get();
loanTransactionHelper.reverseLoanTransaction(loanId.intValue(), repaymentId, "10 January 2021", responseSpec);
logLoanTransactions(loanId);
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), //
reversedTransaction(85.63, "Repayment", "10 January 2021"), //
transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), //
transaction(5.70, "Interest Refund", "22 January 2021"), //
transaction(5.70, "Accrual", "10 January 2021") //
);
});
}
private void logInstallmentsOfLoanDetails(GetLoansLoanIdResponse loanDetails) { private void logInstallmentsOfLoanDetails(GetLoansLoanIdResponse loanDetails) {
log.info("index, dueDate, principal, fee, penalty, interest"); log.info("index, dueDate, principal, fee, penalty, interest");
if (loanDetails != null && loanDetails.getRepaymentSchedule() != null && loanDetails.getRepaymentSchedule().getPeriods() != null) { if (loanDetails != null && loanDetails.getRepaymentSchedule() != null && loanDetails.getRepaymentSchedule().getPeriods() != null) {