FINERACT-2060: re-amortization
This commit is contained in:
+49
@@ -18,9 +18,11 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
|
||||||
|
|
||||||
|
import static java.math.BigDecimal.ZERO;
|
||||||
import static java.util.stream.Collectors.mapping;
|
import static java.util.stream.Collectors.mapping;
|
||||||
import static java.util.stream.Collectors.toList;
|
import static java.util.stream.Collectors.toList;
|
||||||
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.CHARGEBACK;
|
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.CHARGEBACK;
|
||||||
|
import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REAMORTIZE;
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE;
|
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE;
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
|
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
|
||||||
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
|
import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
|
||||||
@@ -182,6 +184,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(),
|
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getCharges(),
|
||||||
ctx.getOverpaymentHolder());
|
ctx.getOverpaymentHolder());
|
||||||
case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed.");
|
case WAIVE_CHARGES -> log.debug("WAIVE_CHARGES transaction will not be processed.");
|
||||||
|
case REAMORTIZE -> handleReAmortization(loanTransaction, ctx.getCurrency(), ctx.getInstallments());
|
||||||
// TODO: Cover rest of the transaction types
|
// TODO: Cover rest of the transaction types
|
||||||
default -> {
|
default -> {
|
||||||
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
|
log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf());
|
||||||
@@ -189,6 +192,52 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleReAmortization(LoanTransaction loanTransaction, MonetaryCurrency currency,
|
||||||
|
List<LoanRepaymentScheduleInstallment> installments) {
|
||||||
|
BigDecimal remainingAmount = loanTransaction.getAmount();
|
||||||
|
LocalDate transactionDate = loanTransaction.getTransactionDate();
|
||||||
|
List<LoanRepaymentScheduleInstallment> previousInstallments = installments.stream() //
|
||||||
|
.filter(installment -> !installment.getDueDate().isAfter(transactionDate)) //
|
||||||
|
.toList();
|
||||||
|
List<LoanRepaymentScheduleInstallment> futureInstallments = installments.stream() //
|
||||||
|
.filter(installment -> installment.getDueDate().isAfter(transactionDate)) //
|
||||||
|
.filter(installment -> !installment.isAdditional() && !installment.isDownPayment()) //
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
BigDecimal overallOverDuePrincipal = ZERO;
|
||||||
|
for (LoanRepaymentScheduleInstallment installment : previousInstallments) {
|
||||||
|
Money principalCompleted = installment.getPrincipalCompleted(currency);
|
||||||
|
overallOverDuePrincipal = overallOverDuePrincipal.add(installment.getPrincipal(currency).minus(principalCompleted).getAmount());
|
||||||
|
installment.updatePrincipal(installment.getPrincipalCompleted(currency).getAmount());
|
||||||
|
installment.updateDerivedFields(currency, transactionDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overallOverDuePrincipal.compareTo(remainingAmount) != 0) {
|
||||||
|
remainingAmount = overallOverDuePrincipal;
|
||||||
|
loanTransaction.updateComponentsAndTotal(Money.of(currency, remainingAmount), Money.zero(currency), Money.zero(currency),
|
||||||
|
Money.zero(currency));
|
||||||
|
}
|
||||||
|
|
||||||
|
LoanRepaymentScheduleInstallment lastFutureInstallment = futureInstallments.stream()
|
||||||
|
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate)).get();
|
||||||
|
BigDecimal reAmortizationAmountPerInstallment = remainingAmount.divide(BigDecimal.valueOf(futureInstallments.size()),
|
||||||
|
MoneyHelper.getRoundingMode());
|
||||||
|
Integer installmentAmountInMultiplesOf = loanTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf();
|
||||||
|
|
||||||
|
for (LoanRepaymentScheduleInstallment installment : futureInstallments) {
|
||||||
|
if (lastFutureInstallment.equals(installment)) {
|
||||||
|
installment.addToPrincipal(transactionDate, Money.of(currency, remainingAmount));
|
||||||
|
} else {
|
||||||
|
if (installmentAmountInMultiplesOf != null) {
|
||||||
|
reAmortizationAmountPerInstallment = Money.roundToMultiplesOf(reAmortizationAmountPerInstallment,
|
||||||
|
installmentAmountInMultiplesOf);
|
||||||
|
}
|
||||||
|
installment.addToPrincipal(transactionDate, Money.of(currency, reAmortizationAmountPerInstallment));
|
||||||
|
remainingAmount = remainingAmount.subtract(reAmortizationAmountPerInstallment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void handleChargeback(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
protected void handleChargeback(LoanTransaction loanTransaction, TransactionCtx ctx) {
|
||||||
processCreditTransaction(loanTransaction, ctx);
|
processCreditTransaction(loanTransaction, ctx);
|
||||||
|
|||||||
+31
@@ -22,8 +22,10 @@ import static java.math.BigDecimal.ZERO;
|
|||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
@@ -40,9 +42,13 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
|
|||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.portfolio.loanaccount.api.LoanReAmortizationApiConstants;
|
import org.apache.fineract.portfolio.loanaccount.api.LoanReAmortizationApiConstants;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -57,6 +63,7 @@ public class LoanReAmortizationServiceImpl {
|
|||||||
private final ExternalIdFactory externalIdFactory;
|
private final ExternalIdFactory externalIdFactory;
|
||||||
private final BusinessEventNotifierService businessEventNotifierService;
|
private final BusinessEventNotifierService businessEventNotifierService;
|
||||||
private final LoanTransactionRepository loanTransactionRepository;
|
private final LoanTransactionRepository loanTransactionRepository;
|
||||||
|
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
|
||||||
|
|
||||||
public CommandProcessingResult reAmortize(Long loanId, JsonCommand command) {
|
public CommandProcessingResult reAmortize(Long loanId, JsonCommand command) {
|
||||||
Loan loan = loanAssembler.assembleFrom(loanId);
|
Loan loan = loanAssembler.assembleFrom(loanId);
|
||||||
@@ -67,6 +74,13 @@ public class LoanReAmortizationServiceImpl {
|
|||||||
changes.put(LoanReAmortizationApiConstants.dateFormatParameterName, command.dateFormat());
|
changes.put(LoanReAmortizationApiConstants.dateFormatParameterName, command.dateFormat());
|
||||||
|
|
||||||
LoanTransaction reAmortizeTransaction = createReAmortizeTransaction(loan, command);
|
LoanTransaction reAmortizeTransaction = createReAmortizeTransaction(loan, command);
|
||||||
|
|
||||||
|
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
||||||
|
.determineProcessor(loan.transactionProcessingStrategy());
|
||||||
|
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAmortizeTransaction,
|
||||||
|
new LoanRepaymentScheduleTransactionProcessor.TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
|
||||||
|
loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney())));
|
||||||
|
|
||||||
loanTransactionRepository.saveAndFlush(reAmortizeTransaction);
|
loanTransactionRepository.saveAndFlush(reAmortizeTransaction);
|
||||||
|
|
||||||
// delinquency recalculation will be triggered by the event in a decoupled way via a listener
|
// delinquency recalculation will be triggered by the event in a decoupled way via a listener
|
||||||
@@ -117,6 +131,23 @@ public class LoanReAmortizationServiceImpl {
|
|||||||
LoanReAmortizationApiConstants.externalIdParameterName);
|
LoanReAmortizationApiConstants.externalIdParameterName);
|
||||||
reAmortizeTransaction.reverse(reversalExternalId);
|
reAmortizeTransaction.reverse(reversalExternalId);
|
||||||
reAmortizeTransaction.manuallyAdjustedOrReversed();
|
reAmortizeTransaction.manuallyAdjustedOrReversed();
|
||||||
|
reProcessLoanTransactions(reAmortizeTransaction.getLoan());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reProcessLoanTransactions(Loan loan) {
|
||||||
|
final List<LoanTransaction> repaymentsOrWaivers = new ArrayList<>();
|
||||||
|
List<LoanTransaction> trans = loan.getLoanTransactions();
|
||||||
|
for (final LoanTransaction transaction : trans) {
|
||||||
|
if (transaction.isNotReversed() && (transaction.isChargeOff() || !transaction.isNonMonetaryTransaction())) {
|
||||||
|
repaymentsOrWaivers.add(transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
repaymentsOrWaivers.sort(LoanTransactionComparator.INSTANCE);
|
||||||
|
|
||||||
|
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
|
||||||
|
.determineProcessor(loan.transactionProcessingStrategy());
|
||||||
|
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(), repaymentsOrWaivers,
|
||||||
|
loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
|
||||||
}
|
}
|
||||||
|
|
||||||
private LoanTransaction findLatestNonReversedReAmortizeTransaction(Loan loan) {
|
private LoanTransaction findLatestNonReversedReAmortizeTransaction(Loan loan) {
|
||||||
|
|||||||
+505
-2
@@ -18,8 +18,11 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.integrationtests.loan.reamortization;
|
package org.apache.fineract.integrationtests.loan.reamortization;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
|
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
|
||||||
@@ -28,10 +31,17 @@ import org.apache.fineract.client.models.PostLoansResponse;
|
|||||||
import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
|
import org.apache.fineract.integrationtests.BaseLoanIntegrationTest;
|
||||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||||
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
||||||
|
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|
||||||
|
@ExtendWith(LoanTestLifecycleExtension.class)
|
||||||
public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
|
public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
|
||||||
|
|
||||||
|
public static final BigDecimal DOWN_PAYMENT_PERCENTAGE = new BigDecimal(25);
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void test_LoanReAmortizeTransaction_Works() {
|
public void test_LoanReAmortizeTransaction_Works() {
|
||||||
AtomicLong createdLoanId = new AtomicLong();
|
AtomicLong createdLoanId = new AtomicLong();
|
||||||
@@ -99,7 +109,11 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
|
|||||||
transaction(625.0, "Re-amortize", "02 February 2023") //
|
transaction(625.0, "Re-amortize", "02 February 2023") //
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: verify installments when schedule generation is implemented
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(0.0, true, "01 February 2023"), //
|
||||||
|
installment(1250.0, false, "01 March 2023") //
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +197,496 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest {
|
|||||||
reversedTransaction(625.0, "Re-amortize", "02 February 2023") //
|
reversedTransaction(625.0, "Re-amortize", "02 February 2023") //
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: verify installments when schedule generation is implemented
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(625.0, false, "01 February 2023"), //
|
||||||
|
installment(625.0, false, "01 March 2023") //
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reAmortizeLoanRepaymentScheduleTest() {
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
Long loanProductId = createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2023", 500.0, 3, req -> {
|
||||||
|
req.setRepaymentEvery(15);
|
||||||
|
req.setLoanTermFrequency(45);
|
||||||
|
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, false, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("05 January 2023");
|
||||||
|
addCharge(loanId, false, 10.0, "05 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, 0.0, 10.0, 135.0, false, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("25 January 2023");
|
||||||
|
|
||||||
|
reAmortizeLoan(loanId);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(0.0, 0.0, 10.0, 10.0, false, "16 January 2023"), //
|
||||||
|
installment(187.5, false, "31 January 2023"), //
|
||||||
|
installment(187.5, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void completePastDueReAmortizationTest() {
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
Long loanProductId = createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2023", 500.0, 3, req -> {
|
||||||
|
req.setRepaymentEvery(15);
|
||||||
|
req.setLoanTermFrequency(45);
|
||||||
|
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, false, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("01 February 2023");
|
||||||
|
|
||||||
|
reAmortizeLoan(loanId);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(0.0, true, "16 January 2023"), //
|
||||||
|
installment(0.0, true, "31 January 2023"), //
|
||||||
|
installment(375.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void partiallyPaidReAmortizationTest() {
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
Long loanProductId = createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2023", 500.0, 3, req -> {
|
||||||
|
req.setRepaymentEvery(15);
|
||||||
|
req.setLoanTermFrequency(45);
|
||||||
|
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, false, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("17 January 2023");
|
||||||
|
addRepaymentForLoan(loanId, 50.0, "17 January 2023");
|
||||||
|
|
||||||
|
updateBusinessDate("30 January 2023");
|
||||||
|
|
||||||
|
reAmortizeLoan(loanId);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(50.0, true, "16 January 2023"), //
|
||||||
|
installment(162.5, false, "31 January 2023"), //
|
||||||
|
installment(162.5, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reAmortizationOnSameDayOfInstallmentTest() {
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
Long loanProductId = createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2023", 500.0, 3, req -> {
|
||||||
|
req.setRepaymentEvery(15);
|
||||||
|
req.setLoanTermFrequency(45);
|
||||||
|
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, false, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("05 January 2023");
|
||||||
|
addCharge(loanId, false, 10.0, "05 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, 0.0, 10.0, 135.0, false, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("31 January 2023");
|
||||||
|
|
||||||
|
reAmortizeLoan(loanId);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(0.0, 0.0, 10.0, 10.0, false, "16 January 2023"), //
|
||||||
|
installment(0.0, true, "31 January 2023"), //
|
||||||
|
installment(375.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reAmortizationNPlusOneInstallmentTest() {
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
Long loanProductId = createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2023", 500.0, 3, req -> {
|
||||||
|
req.setRepaymentEvery(15);
|
||||||
|
req.setLoanTermFrequency(45);
|
||||||
|
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, false, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("01 February 2023");
|
||||||
|
addCharge(loanId, false, 10.0, "27 February 2023");
|
||||||
|
|
||||||
|
reAmortizeLoan(loanId);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(0.0, true, "16 January 2023"), //
|
||||||
|
installment(0.0, true, "31 January 2023"), //
|
||||||
|
installment(375.0, false, "15 February 2023"), //
|
||||||
|
installment(0.0, 0.0, 10.0, false, "27 February 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reAmortizationBackdatedRepaymentAndReplayTest() {
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
Long loanProductId = createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2023", 500.0, 3, req -> {
|
||||||
|
req.setRepaymentEvery(15);
|
||||||
|
req.setLoanTermFrequency(45);
|
||||||
|
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, false, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("01 February 2023");
|
||||||
|
|
||||||
|
reAmortizeLoan(loanId);
|
||||||
|
|
||||||
|
updateBusinessDate("01 February 2023");
|
||||||
|
addRepaymentForLoan(loanId, 125.0, "15 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "16 January 2023"), //
|
||||||
|
installment(0.0, true, "31 January 2023"), //
|
||||||
|
installment(250.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reAmortizationUndoRepaymentAndReplayTest() {
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
Long loanProductId = createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2023", 500.0, 3, req -> {
|
||||||
|
req.setRepaymentEvery(15);
|
||||||
|
req.setLoanTermFrequency(45);
|
||||||
|
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, false, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("15 January 2023");
|
||||||
|
Long repaymentTransactionId = addRepaymentForLoan(loanId, 125.0, "15 January 2023");
|
||||||
|
|
||||||
|
updateBusinessDate("01 February 2023");
|
||||||
|
|
||||||
|
reAmortizeLoan(loanId);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "16 January 2023"), //
|
||||||
|
installment(0.0, true, "31 January 2023"), //
|
||||||
|
installment(250.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
|
||||||
|
loanTransactionHelper.reverseRepayment(loanId.intValue(), repaymentTransactionId.intValue(), "01 February 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(0.0, true, "16 January 2023"), //
|
||||||
|
installment(0.0, true, "31 January 2023"), //
|
||||||
|
installment(375.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reverseReAmortizationTest() {
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
Long loanProductId = createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2023", 500.0, 3, req -> {
|
||||||
|
req.setRepaymentEvery(15);
|
||||||
|
req.setLoanTermFrequency(45);
|
||||||
|
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, false, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("01 February 2023");
|
||||||
|
|
||||||
|
reAmortizeLoan(loanId);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(0.0, true, "16 January 2023"), //
|
||||||
|
installment(0.0, true, "31 January 2023"), //
|
||||||
|
installment(375.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("02 February 2023");
|
||||||
|
|
||||||
|
undoReAmortizeLoan(loanId);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, false, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023")//
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void reAmortizationDivisionTest() {
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
Long loanProductId = createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(4, 15, BigDecimal.valueOf(20));
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2023", 500.0, 4, req -> {
|
||||||
|
req.setRepaymentEvery(15);
|
||||||
|
req.setLoanTermFrequency(60);
|
||||||
|
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(100.0, true, "01 January 2023"), //
|
||||||
|
installment(100.0, false, "16 January 2023"), //
|
||||||
|
installment(100.0, false, "31 January 2023"), //
|
||||||
|
installment(100.0, false, "15 February 2023"), //
|
||||||
|
installment(100.0, false, "02 March 2023")//
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("17 January 2023");
|
||||||
|
|
||||||
|
reAmortizeLoan(loanId);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(100.0, true, "01 January 2023"), //
|
||||||
|
installment(0.0, true, "16 January 2023"), //
|
||||||
|
installment(133.33, false, "31 January 2023"), //
|
||||||
|
installment(133.33, false, "15 February 2023"), //
|
||||||
|
installment(133.34, false, "02 March 2023")//
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void secondDisbursementAfterReAmortizationTest() {
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
Long loanProductId = createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(3, 15);
|
||||||
|
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2023", 1000.0, 3, req -> {
|
||||||
|
req.setRepaymentEvery(15);
|
||||||
|
req.setLoanTermFrequency(45);
|
||||||
|
req.setTransactionProcessingStrategyCode("advanced-payment-allocation-strategy");
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleType.PROGRESSIVE.toString());
|
||||||
|
req.setLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(500.00), "01 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(125.0, false, "16 January 2023"), //
|
||||||
|
installment(125.0, false, "31 January 2023"), //
|
||||||
|
installment(125.0, false, "15 February 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("16 January 2023");
|
||||||
|
addCharge(loanId, false, 10.0, "16 January 2023");
|
||||||
|
|
||||||
|
updateBusinessDate("25 January 2023");
|
||||||
|
reAmortizeLoan(loanId);
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(0.0, 0.0, 10.0, 10.0, false, "16 January 2023"), //
|
||||||
|
installment(187.5, false, "31 January 2023"), //
|
||||||
|
installment(187.5, false, "15 February 2023") //
|
||||||
|
);
|
||||||
|
|
||||||
|
updateBusinessDate("26 January 2023");
|
||||||
|
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(500.00), "26 January 2023");
|
||||||
|
|
||||||
|
verifyRepaymentSchedule(loanId, //
|
||||||
|
installment(0, null, "01 January 2023"), //
|
||||||
|
installment(125.0, true, "01 January 2023"), //
|
||||||
|
installment(0.0, 0.0, 10.0, 0.0, true, "16 January 2023"), //
|
||||||
|
installment(500.0, null, "26 January 2023"), //
|
||||||
|
installment(125.0, false, "26 January 2023"), //
|
||||||
|
installment(375.0, false, "31 January 2023"), //
|
||||||
|
installment(375.0, false, "15 February 2023") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(int numberOfInstallments, int repaymentEvery) {
|
||||||
|
return createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(numberOfInstallments, repaymentEvery,
|
||||||
|
DOWN_PAYMENT_PERCENTAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long createLoanProductWithMultiDisbursalAndRepaymentsWithEnableDownPayment(int numberOfInstallments, int repaymentEvery,
|
||||||
|
BigDecimal downPaymentPercentage) {
|
||||||
|
boolean multiDisburseEnabled = true;
|
||||||
|
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation();
|
||||||
|
product.setMultiDisburseLoan(multiDisburseEnabled);
|
||||||
|
product.setNumberOfRepayments(numberOfInstallments);
|
||||||
|
product.setRepaymentEvery(repaymentEvery);
|
||||||
|
|
||||||
|
if (!multiDisburseEnabled) {
|
||||||
|
product.disallowExpectedDisbursements(null);
|
||||||
|
product.setAllowApprovedDisbursedAmountsOverApplied(null);
|
||||||
|
product.overAppliedCalculationType(null);
|
||||||
|
product.overAppliedNumber(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
product.setEnableDownPayment(true);
|
||||||
|
product.setDisbursedAmountPercentageForDownPayment(downPaymentPercentage);
|
||||||
|
product.setEnableAutoRepaymentForDownPayment(true);
|
||||||
|
product.setInstallmentAmountInMultiplesOf(null);
|
||||||
|
|
||||||
|
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||||
|
GetLoanProductsProductIdResponse getLoanProductsProductIdResponse = loanProductHelper
|
||||||
|
.retrieveLoanProductById(loanProductResponse.getResourceId());
|
||||||
|
assertNotNull(getLoanProductsProductIdResponse);
|
||||||
|
return loanProductResponse.getResourceId();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user