diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 60fb6caee..d4a6a9f42 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -18,9 +18,11 @@ */ 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.toList; 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.INTEREST; 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(), ctx.getOverpaymentHolder()); 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 default -> { 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 installments) { + BigDecimal remainingAmount = loanTransaction.getAmount(); + LocalDate transactionDate = loanTransaction.getTransactionDate(); + List previousInstallments = installments.stream() // + .filter(installment -> !installment.getDueDate().isAfter(transactionDate)) // + .toList(); + List 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 protected void handleChargeback(LoanTransaction loanTransaction, TransactionCtx ctx) { processCreditTransaction(loanTransaction, ctx); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java index c3ff06eb6..b12af758f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java @@ -22,8 +22,10 @@ import static java.math.BigDecimal.ZERO; import java.math.BigDecimal; import java.time.LocalDate; +import java.util.ArrayList; import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import lombok.RequiredArgsConstructor; 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.portfolio.loanaccount.api.LoanReAmortizationApiConstants; 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.LoanTransactionComparator; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; 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.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -57,6 +63,7 @@ public class LoanReAmortizationServiceImpl { private final ExternalIdFactory externalIdFactory; private final BusinessEventNotifierService businessEventNotifierService; private final LoanTransactionRepository loanTransactionRepository; + private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; public CommandProcessingResult reAmortize(Long loanId, JsonCommand command) { Loan loan = loanAssembler.assembleFrom(loanId); @@ -67,6 +74,13 @@ public class LoanReAmortizationServiceImpl { changes.put(LoanReAmortizationApiConstants.dateFormatParameterName, command.dateFormat()); 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); // delinquency recalculation will be triggered by the event in a decoupled way via a listener @@ -117,6 +131,23 @@ public class LoanReAmortizationServiceImpl { LoanReAmortizationApiConstants.externalIdParameterName); reAmortizeTransaction.reverse(reversalExternalId); reAmortizeTransaction.manuallyAdjustedOrReversed(); + reProcessLoanTransactions(reAmortizeTransaction.getLoan()); + } + + private void reProcessLoanTransactions(Loan loan) { + final List repaymentsOrWaivers = new ArrayList<>(); + List 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) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java index d902baf85..eae605740 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reamortization/LoanReAmortizationIntegrationTest.java @@ -18,8 +18,11 @@ */ package org.apache.fineract.integrationtests.loan.reamortization; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import java.math.BigDecimal; 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.PostLoanProductsResponse; 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.common.ClientHelper; 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.extension.ExtendWith; +@ExtendWith(LoanTestLifecycleExtension.class) public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest { + public static final BigDecimal DOWN_PAYMENT_PERCENTAGE = new BigDecimal(25); + @Test public void test_LoanReAmortizeTransaction_Works() { AtomicLong createdLoanId = new AtomicLong(); @@ -99,7 +109,11 @@ public class LoanReAmortizationIntegrationTest extends BaseLoanIntegrationTest { 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") // ); - // 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(); + } }