FINERACT-1968: Advanced payment allocation - Default allocation

This commit is contained in:
Adam Saghy
2023-09-05 17:14:32 +02:00
parent 26eca0fccd
commit 6d938995a0
14 changed files with 2163 additions and 47 deletions
@@ -25,6 +25,7 @@ import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@@ -40,7 +41,8 @@ public class DefaultExceptionMapper implements FineractExceptionMapper, Exceptio
@Override
public Response toResponse(RuntimeException runtimeException) {
return Response.status(SC_INTERNAL_SERVER_ERROR).entity(Map.of("Exception", runtimeException.getMessage()))
return Response.status(SC_INTERNAL_SERVER_ERROR)
.entity(Map.of("Exception", ObjectUtils.defaultIfNull(runtimeException.getMessage(), "No error message available")))
.type(MediaType.APPLICATION_JSON).build();
}
}
@@ -433,6 +433,14 @@ public final class MathUtil {
return min(min(first, second, notNull), third, notNull);
}
/**
* Calculate percentage of a value
*
* @param value
* @param percentage
* @param precision
* @return
*/
public static BigDecimal percentageOf(final BigDecimal value, final BigDecimal percentage, final int precision) {
BigDecimal percentageOf = BigDecimal.ZERO;
if (isGreaterThanZero(value)) {
@@ -702,7 +702,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(),
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
@@ -863,7 +863,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
* affected Transactions
***/
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(),
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
}
this.charges.remove(loanCharge);
@@ -931,7 +931,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
* affected Transactions
***/
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(),
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
} else {
// reprocess loan schedule based on charge been waived.
@@ -1124,7 +1124,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
* affected Transactions
***/
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(),
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
} else {
// reprocess loan schedule based on charge been waived.
@@ -2706,7 +2706,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
if (!allNonContraTransactionsPostDisbursement.isEmpty()) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(),
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
@@ -3351,7 +3351,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
}
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(),
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
@@ -3779,7 +3779,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
}
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
getActiveCharges());
updateLoanSummaryDerivedFields();
@@ -3928,7 +3928,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
}
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(),
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
@@ -5402,7 +5402,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
@@ -5545,7 +5545,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(),
getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
@@ -5788,7 +5788,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
for (LoanTransaction loanTransaction : allNonContraTransactionsPostDisbursement) {
copyTransactions.add(LoanTransaction.copyTransactionProperties(loanTransaction));
}
loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(), copyTransactions, getCurrency(),
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), copyTransactions, getCurrency(),
getRepaymentScheduleInstallments(), getActiveCharges());
updateLoanSummaryDerivedFields();
@@ -6392,7 +6392,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
getRepaymentScheduleInstallments(), getActiveCharges(), getTotalOverpaidAsMoney());
} else {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement();
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(),
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
@@ -49,7 +49,7 @@ public interface LoanRepaymentScheduleTransactionProcessor {
* before existing transactions or and adjustment is made to an existing in which case the entire loan schedule
* needs to be re-processed.
*/
ChangedTransactionDetail reprocessLoanTranactions(LocalDate disbursementDate, List<LoanTransaction> repaymentsOrWaivers,
ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List<LoanTransaction> repaymentsOrWaivers,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, Set<LoanCharge> charges);
Money handleRepaymentSchedule(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
@@ -27,7 +27,15 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
public enum PaymentAllocationTransactionType {
DEFAULT(null), //
REPAYMENT(LoanTransactionType.REPAYMENT); //
REPAYMENT(LoanTransactionType.REPAYMENT), //
DOWN_PAYMENT(LoanTransactionType.DOWN_PAYMENT), //
MERCHANT_ISSUED_REFUND(LoanTransactionType.MERCHANT_ISSUED_REFUND), //
PAYOUT_REFUND(LoanTransactionType.PAYOUT_REFUND), //
GOODWILL_CREDIT(LoanTransactionType.GOODWILL_CREDIT), //
CHARGE_REFUND(LoanTransactionType.CHARGE_REFUND), //
CHARGE_ADJUSTMENT(LoanTransactionType.CHARGE_ADJUSTMENT), //
WAIVE_INTEREST(LoanTransactionType.WAIVE_INTEREST),//
; //
private final LoanTransactionType loanTransactionType;
}
@@ -64,7 +64,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
@Override
public ChangedTransactionDetail reprocessLoanTranactions(final LocalDate disbursementDate,
public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbursementDate,
final List<LoanTransaction> transactionsPostDisbursement, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges) {
@@ -382,7 +382,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
}
private void reprocessInstallments(List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency) {
protected void reprocessInstallments(List<LoanRepaymentScheduleInstallment> installments, MonetaryCurrency currency) {
LoanRepaymentScheduleInstallment lastInstallment = installments.get(installments.size() - 1);
if (lastInstallment.isAdditional() && lastInstallment.getDue(currency).isZero()) {
installments.remove(lastInstallment);
@@ -413,7 +413,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
return mergedList;
}
private void createNewTransaction(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction,
protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction,
ChangedTransactionDetail changedTransactionDetail) {
loanTransaction.reverse();
loanTransaction.updateExternalId(null);
@@ -538,7 +538,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
}
private Money handleTransactionAndCharges(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
protected Money handleTransactionAndCharges(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, final Money chargeAmountToProcess,
final boolean isFeeCharge) {
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
@@ -574,7 +574,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
return transactionAmountUnprocessed;
}
private Money processTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
protected Money processTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges, Money amountToProcess) {
int installmentIndex = 0;
@@ -607,7 +607,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
return transactionAmountUnprocessed;
}
private Set<LoanCharge> extractFeeCharges(final Set<LoanCharge> loanCharges) {
protected Set<LoanCharge> extractFeeCharges(final Set<LoanCharge> loanCharges) {
final Set<LoanCharge> feeCharges = new HashSet<>();
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isFeeCharge()) {
@@ -617,7 +617,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
return feeCharges;
}
private Set<LoanCharge> extractPenaltyCharges(final Set<LoanCharge> loanCharges) {
protected Set<LoanCharge> extractPenaltyCharges(final Set<LoanCharge> loanCharges) {
final Set<LoanCharge> penaltyCharges = new HashSet<>();
for (final LoanCharge loanCharge : loanCharges) {
if (loanCharge.isPenaltyCharge()) {
@@ -627,7 +627,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
return penaltyCharges;
}
private void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> charges,
protected void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> charges,
final Integer installmentNumber) {
Money amountRemaining = feeCharges;
@@ -661,7 +661,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
private LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(final Set<LoanCharge> charges, final MonetaryCurrency currency) {
protected LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(final Set<LoanCharge> charges, final MonetaryCurrency currency) {
LoanCharge earliestUnpaidCharge = null;
LoanCharge installemntCharge = null;
LoanInstallmentCharge chargePerInstallment = null;
@@ -687,7 +687,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
return earliestUnpaidCharge;
}
private void handleWriteOff(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
protected void handleWriteOff(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
@@ -712,12 +712,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
}
private void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount,
protected void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount,
List<LoanRepaymentScheduleInstallment> installments) {
processCreditTransaction(loanTransaction, overpaidAmount, currency, installments);
}
private void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, final Set<LoanCharge> charges) {
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
final Comparator<LoanRepaymentScheduleInstallment> byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
@@ -18,19 +18,37 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
import org.springframework.context.annotation.Profile;
//TODO: remove `test` profile when it is finished
@Profile("test")
@Slf4j
public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
@@ -74,20 +92,285 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
}
@Override
public ChangedTransactionDetail reprocessLoanTranactions(LocalDate disbursementDate, List<LoanTransaction> transactionsPostDisbursement,
MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
throw new NotImplementedException();
public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate,
List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
// TODO: rewrite this whole logic step by step
if (charges != null) {
for (final LoanCharge loanCharge : charges) {
if (!loanCharge.isDueAtDisbursement()) {
loanCharge.resetPaidAmount(currency);
}
}
}
for (final LoanRepaymentScheduleInstallment currentInstallment : installments) {
currentInstallment.resetDerivedComponents();
currentInstallment.updateDerivedFields(currency, disbursementDate);
}
// TODO: Remove this reprocess and add the charges to the installment in chronological order
final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper();
wrapper.reprocess(currency, disbursementDate, installments, charges);
final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail();
for (final LoanTransaction loanTransaction : transactionsPostDisbursement) {
if (loanTransaction.getId() == null) {
processLatestTransaction(loanTransaction, currency, installments, charges, null);
loanTransaction.adjustInterestComponent(currency);
} else {
/**
* For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) has
* changed.<br>
**/
final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction);
// Reset derived component of new loan transaction and
// re-process transaction
processLatestTransaction(newLoanTransaction, currency, installments, charges, null);
newLoanTransaction.adjustInterestComponent(currency);
/**
* Check if the transaction amounts have changed. If so, reverse the original transaction and update
* changedTransactionDetail accordingly
**/
if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) {
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(
newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings());
} else {
createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail);
}
}
}
reprocessInstallments(installments, currency);
return changedTransactionDetail;
}
@Override
public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges, Money overpaidAmount) {
throw new NotImplementedException();
switch (loanTransaction.getTypeOf()) {
case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments);
case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges);
case CHARGEBACK -> handleChargeback(loanTransaction, currency, overpaidAmount, installments);
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
WAIVE_INTEREST, RECOVERY_REPAYMENT ->
handleRepayment(loanTransaction, currency, installments, charges);
// TODO: Cover rest of the transaction types
default -> {
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
}
}
}
@Override
public Money handleRepaymentSchedule(List<LoanTransaction> transactionsPostDisbursement, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> loanCharges) {
return super.handleRepaymentSchedule(transactionsPostDisbursement, currency, installments, loanCharges);
throw new NotImplementedException();
}
private void handleRepayment(LoanTransaction loanTransaction, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges) {
if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
loanTransaction.resetDerivedComponents();
}
Money transactionAmountUnprocessed = loanTransaction.getAmount(currency);
Money zero = Money.zero(currency);
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings = new ArrayList<>();
List<LoanPaymentAllocationRule> paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules();
LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream()
.filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream()
.filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst()
.orElse(defaultPaymentAllocationRule);
Balances balances = new Balances(zero, zero, zero, zero);
for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) {
FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule();
LoanRepaymentScheduleInstallment currentInstallment = null;
Money paidPortion = zero;
do {
switch (paymentAllocationType.getDueType()) {
case PAST_DUE -> {
currentInstallment = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.getTransactionDate().isAfter(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
if (currentInstallment != null) {
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency);
paidPortion = payAllocation(paymentAllocationType, currentInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
}
}
case DUE -> {
currentInstallment = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.getTransactionDate().isEqual(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
if (currentInstallment != null) {
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency);
paidPortion = payAllocation(paymentAllocationType, currentInstallment, loanTransaction,
transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances);
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion);
}
}
case IN_ADVANCE -> {
// For having similar logic we are populating installment list even when the future installment
// allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element.
List<LoanRepaymentScheduleInstallment> currentInstallments = new ArrayList<>();
if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) {
currentInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.getTransactionDate().isBefore(e.getDueDate())).toList();
} else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
currentInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.getTransactionDate().isBefore(e.getDueDate()))
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
} else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) {
currentInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff)
.filter(e -> loanTransaction.getTransactionDate().isBefore(e.getDueDate()))
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
}
int numberOfInstallments = currentInstallments.size();
paidPortion = zero;
if (numberOfInstallments > 0) {
// This will be the same amount as transactionAmountUnprocessed in case of the future
// installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT
Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode());
// Adjustment might be needed due to the divide operation and the rounding mode
Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) {
currentInstallment = internalCurrentInstallment;
// Adjust the portion for the last installment
if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) {
evenPortion = evenPortion.add(balanceAdjustment);
}
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
transactionMappings, loanTransaction, currentInstallment, currency);
Money internalPaidPortion = payAllocation(paymentAllocationType, currentInstallment, loanTransaction,
evenPortion, loanTransactionToRepaymentScheduleMapping, balances);
// Some extra logic to allocate as much as possible across the installments if the
// outstanding balances are different
if (internalPaidPortion.isGreaterThanZero()) {
paidPortion = internalPaidPortion;
}
transactionAmountUnprocessed = transactionAmountUnprocessed.minus(internalPaidPortion);
}
} else {
currentInstallment = null;
}
}
}
}
// We are allocating till there is no pending installment or there is no more unprocessed transaction amount
// or there is no more outstanding balance of the allocation type
while (currentInstallment != null && transactionAmountUnprocessed.isGreaterThanZero() && paidPortion.isGreaterThanZero());
}
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(),
balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion());
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
// TODO: rewrite to provide sorted list
final Set<LoanCharge> loanFees = extractFeeCharges(charges);
final Set<LoanCharge> loanPenalties = extractPenaltyCharges(charges);
if (loanTransaction.isNotWaiver() && !loanTransaction.isAccrual()) {
Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
if (feeCharges.isGreaterThanZero()) {
// TODO: Rewrite to exclude charge payment
updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, null);
}
if (penaltyCharges.isGreaterThanZero()) {
// TODO: Rewrite to exclude charge payment
updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, null);
}
}
handleOverpayment(transactionAmountUnprocessed, loanTransaction);
}
private LoanTransactionToRepaymentScheduleMapping getTransactionMapping(
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings, LoanTransaction loanTransaction,
LoanRepaymentScheduleInstallment currentInstallment, MonetaryCurrency currency) {
Money zero = Money.zero(currency);
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = transactionMappings.stream()
.filter(e -> loanTransaction.equals(e.getLoanTransaction()))
.filter(e -> currentInstallment.equals(e.getLoanRepaymentScheduleInstallment())).findFirst().orElse(null);
if (loanTransactionToRepaymentScheduleMapping == null) {
loanTransactionToRepaymentScheduleMapping = LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
currentInstallment, zero, zero, zero, zero);
transactionMappings.add(loanTransactionToRepaymentScheduleMapping);
}
return loanTransactionToRepaymentScheduleMapping;
}
private Money payAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment,
LoanTransaction loanTransaction, Money transactionAmountUnprocessed,
LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Balances balances) {
LocalDate transactionDate = loanTransaction.getTransactionDate();
Money zero = transactionAmountUnprocessed.zero();
return switch (paymentAllocationType.getAllocationType()) {
case PENALTY -> {
Money subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountUnprocessed);
balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(subPenaltyPortion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, subPenaltyPortion);
yield subPenaltyPortion;
}
case FEE -> {
Money subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountUnprocessed);
balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().add(subFeePortion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, subFeePortion, zero);
yield subFeePortion;
}
case INTEREST -> {
Money subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountUnprocessed);
balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().add(subInterestPortion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, subInterestPortion, zero, zero);
yield subInterestPortion;
}
case PRINCIPAL -> {
Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountUnprocessed);
balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().add(subPrincipalPortion));
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, subPrincipalPortion, zero, zero, zero);
yield subPrincipalPortion;
}
};
}
private void addToTransactionMapping(LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping,
Money principalPortion, Money interestPortion, Money feePortion, Money penaltyPortion) {
BigDecimal aggregatedPenalty = ObjectUtils
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getPenaltyChargesPortion(), BigDecimal.ZERO)
.add(penaltyPortion.getAmount());
BigDecimal aggregatedFee = ObjectUtils
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getFeeChargesPortion(), BigDecimal.ZERO)
.add(feePortion.getAmount());
BigDecimal aggregatedInterest = ObjectUtils
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getInterestPortion(), BigDecimal.ZERO)
.add(interestPortion.getAmount());
BigDecimal aggregatedPrincipal = ObjectUtils
.defaultIfNull(loanTransactionToRepaymentScheduleMapping.getPrincipalPortion(), BigDecimal.ZERO)
.add(principalPortion.getAmount());
loanTransactionToRepaymentScheduleMapping.setComponents(aggregatedPrincipal, aggregatedInterest, aggregatedFee, aggregatedPenalty);
}
private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction) {
if (overpaymentPortion.isGreaterThanZero()) {
onLoanOverpayment(loanTransaction, overpaymentPortion);
loanTransaction.updateOverPayments(overpaymentPortion);
}
}
@AllArgsConstructor
@Getter
@Setter
private static class Balances {
private Money aggregatedPrincipalPortion;
private Money aggregatedFeeChargesPortion;
private Money aggregatedInterestPortion;
private Money aggregatedPenaltyChargesPortion;
}
}
@@ -100,7 +100,7 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio
boolean ignoreDueDateCheck = false;
boolean rerun = false;
List<LoanCharge> orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isNotFullyPaid)
List<LoanCharge> orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isChargePending)
.filter(loanCharge -> loanCharge.getEffectiveDueDate() == null
|| !loanCharge.getEffectiveDueDate().isAfter(transactionDate))
.sorted(LoanChargeEffectiveDueDateComparator.INSTANCE).toList();
@@ -100,7 +100,7 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio
boolean ignoreDueDateCheck = false;
boolean rerun = false;
List<LoanCharge> orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isNotFullyPaid)
List<LoanCharge> orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isChargePending)
.filter(loanCharge -> loanCharge.getEffectiveDueDate() == null
|| !loanCharge.getEffectiveDueDate().isAfter(transactionDate))
.sorted(LoanChargeEffectiveDueDateComparator.INSTANCE).toList();
@@ -2808,7 +2808,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener
loanRepaymentScheduleTransactionProcessor, onDate, calculateTill);
List<LoanTransaction> loanTransactions = loan.retrieveListOfTransactionsPostDisbursementExcludeAccruals();
loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(loanApplicationTerms.getExpectedDisbursementDate(),
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loanApplicationTerms.getExpectedDisbursementDate(),
loanTransactions, currency, loanScheduleDTO.getInstallments(), loan.getActiveCharges());
Money feeCharges = Money.zero(currency);
Money penaltyCharges = Money.zero(currency);
@@ -437,13 +437,13 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio
LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class);
Mockito.when(loanCharge1.isActive()).thenReturn(true);
Mockito.when(loanCharge1.isNotFullyPaid()).thenReturn(true);
Mockito.when(loanCharge1.isChargePending()).thenReturn(true);
Mockito.when(loanCharge1.getEffectiveDueDate()).thenReturn(transactionDate);
Mockito.when(loanCharge1.isPenaltyCharge()).thenReturn(true);
Mockito.when(loanCharge1.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(two);
LoanCharge loanCharge2 = Mockito.mock(LoanCharge.class);
Mockito.when(loanCharge2.isActive()).thenReturn(true);
Mockito.when(loanCharge2.isNotFullyPaid()).thenReturn(true);
Mockito.when(loanCharge2.isChargePending()).thenReturn(true);
Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.plusDays(1));
Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2));
@@ -478,13 +478,13 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio
LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class);
Mockito.when(loanCharge1.isActive()).thenReturn(true);
Mockito.when(loanCharge1.isNotFullyPaid()).thenReturn(true);
Mockito.when(loanCharge1.isChargePending()).thenReturn(true);
Mockito.when(loanCharge1.getEffectiveDueDate()).thenReturn(transactionDate);
Mockito.when(loanCharge1.isPenaltyCharge()).thenReturn(true);
Mockito.when(loanCharge1.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(two);
LoanCharge loanCharge2 = Mockito.mock(LoanCharge.class);
Mockito.when(loanCharge2.isActive()).thenReturn(true);
Mockito.when(loanCharge2.isNotFullyPaid()).thenReturn(true);
Mockito.when(loanCharge2.isChargePending()).thenReturn(true);
Mockito.when(loanCharge2.isPenaltyCharge()).thenReturn(false);
Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.minusDays(1));
Mockito.when(loanCharge2.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(one);
@@ -521,13 +521,13 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio
LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class);
Mockito.when(loanCharge1.isActive()).thenReturn(true);
Mockito.when(loanCharge1.isNotFullyPaid()).thenReturn(true);
Mockito.when(loanCharge1.isChargePending()).thenReturn(true);
Mockito.when(loanCharge1.getEffectiveDueDate()).thenReturn(transactionDate);
Mockito.when(loanCharge1.isPenaltyCharge()).thenReturn(true);
Mockito.when(loanCharge1.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(eleven);
LoanCharge loanCharge2 = Mockito.mock(LoanCharge.class);
Mockito.when(loanCharge2.isActive()).thenReturn(true);
Mockito.when(loanCharge2.isNotFullyPaid()).thenReturn(true);
Mockito.when(loanCharge2.isChargePending()).thenReturn(true);
Mockito.when(loanCharge2.isPenaltyCharge()).thenReturn(false);
Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.minusDays(1));
Mockito.when(loanCharge2.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(eleven);
@@ -405,13 +405,13 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio
LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class);
Mockito.when(loanCharge1.isActive()).thenReturn(true);
Mockito.when(loanCharge1.isNotFullyPaid()).thenReturn(true);
Mockito.when(loanCharge1.isChargePending()).thenReturn(true);
Mockito.when(loanCharge1.getEffectiveDueDate()).thenReturn(transactionDate);
Mockito.when(loanCharge1.isPenaltyCharge()).thenReturn(true);
Mockito.when(loanCharge1.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(two);
LoanCharge loanCharge2 = Mockito.mock(LoanCharge.class);
Mockito.when(loanCharge2.isActive()).thenReturn(true);
Mockito.when(loanCharge2.isNotFullyPaid()).thenReturn(true);
Mockito.when(loanCharge2.isChargePending()).thenReturn(true);
Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.plusDays(1));
Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2));
@@ -445,13 +445,13 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio
LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class);
Mockito.when(loanCharge1.isActive()).thenReturn(true);
Mockito.when(loanCharge1.isNotFullyPaid()).thenReturn(true);
Mockito.when(loanCharge1.isChargePending()).thenReturn(true);
Mockito.when(loanCharge1.getEffectiveDueDate()).thenReturn(transactionDate);
Mockito.when(loanCharge1.isPenaltyCharge()).thenReturn(true);
Mockito.when(loanCharge1.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(two);
LoanCharge loanCharge2 = Mockito.mock(LoanCharge.class);
Mockito.when(loanCharge2.isActive()).thenReturn(true);
Mockito.when(loanCharge2.isNotFullyPaid()).thenReturn(true);
Mockito.when(loanCharge2.isChargePending()).thenReturn(true);
Mockito.when(loanCharge2.isPenaltyCharge()).thenReturn(false);
Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.minusDays(1));
Mockito.when(loanCharge2.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(one);
@@ -485,13 +485,13 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio
LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class);
Mockito.when(loanCharge1.isActive()).thenReturn(true);
Mockito.when(loanCharge1.isNotFullyPaid()).thenReturn(true);
Mockito.when(loanCharge1.isChargePending()).thenReturn(true);
Mockito.when(loanCharge1.getEffectiveDueDate()).thenReturn(transactionDate);
Mockito.when(loanCharge1.isPenaltyCharge()).thenReturn(true);
Mockito.when(loanCharge1.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(eleven);
LoanCharge loanCharge2 = Mockito.mock(LoanCharge.class);
Mockito.when(loanCharge2.isActive()).thenReturn(true);
Mockito.when(loanCharge2.isNotFullyPaid()).thenReturn(true);
Mockito.when(loanCharge2.isChargePending()).thenReturn(true);
Mockito.when(loanCharge2.isPenaltyCharge()).thenReturn(false);
Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.minusDays(1));
Mockito.when(loanCharge2.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(eleven);
@@ -61,6 +61,8 @@ import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse;
import org.apache.fineract.client.models.GetPaymentTypesResponse;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest;
import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse;
import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest;
@@ -1906,6 +1908,10 @@ public class LoanTransactionHelper extends IntegrationTest {
return ok(fineract().loanProducts.updateLoanProduct(id, requestModifyLoan));
}
public PostLoanProductsResponse createLoanProduct(PostLoanProductsRequest request) {
return ok(fineract().loanProducts.createLoanProduct(request));
}
public PostLoansLoanIdTransactionsResponse makeLoanDownPayment(String loanExternalId, PostLoansLoanIdTransactionsRequest request) {
return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "downPayment"));
}