FINERACT-2181: fix prepayment functionality when prepayment happens in the first month of loan

This commit is contained in:
CsengeSóti
2025-06-19 11:10:36 +02:00
committed by Arnold Gálovics
parent dd2c29fb91
commit cb82af119b
8 changed files with 213 additions and 8 deletions
@@ -35,6 +35,7 @@ public class ScheduleGeneratorDTO {
final CalendarInstance calendarInstanceForInterestRecalculation;
final CalendarInstance compoundingCalendarInstance;
LocalDate recalculateFrom;
LocalDate recalculateTill;
final Long overdurPenaltyWaitPeriod;
final FloatingRateDTO floatingRateDTO;
final Calendar calendar;
@@ -50,8 +51,8 @@ public class ScheduleGeneratorDTO {
public ScheduleGeneratorDTO(final LoanScheduleGeneratorFactory loanScheduleFactory, final CurrencyData currency,
final LocalDate calculatedRepaymentsStartingFromDate, final HolidayDetailDTO holidayDetailDTO,
final CalendarInstance calendarInstanceForInterestRecalculation, final CalendarInstance compoundingCalendarInstance,
final LocalDate recalculateFrom, final Long overdurPenaltyWaitPeriod, final FloatingRateDTO floatingRateDTO,
final Calendar calendar, final CalendarHistoryDataWrapper calendarHistoryDataWrapper,
final LocalDate recalculateFrom, final LocalDate recalculateTill, final Long overdurPenaltyWaitPeriod,
final FloatingRateDTO floatingRateDTO, final Calendar calendar, final CalendarHistoryDataWrapper calendarHistoryDataWrapper,
final Boolean isInterestChargedFromDateAsDisbursementDateEnabled, final Integer numberOfdays,
final boolean isSkipRepaymentOnFirstDayofMonth, final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled,
final boolean isFirstRepaymentDateAllowedOnHoliday, final boolean isInterestToBeRecoveredFirstWhenGreaterThanEMI,
@@ -63,6 +64,7 @@ public class ScheduleGeneratorDTO {
this.calendarInstanceForInterestRecalculation = calendarInstanceForInterestRecalculation;
this.compoundingCalendarInstance = compoundingCalendarInstance;
this.recalculateFrom = recalculateFrom;
this.recalculateTill = recalculateTill;
this.overdurPenaltyWaitPeriod = overdurPenaltyWaitPeriod;
this.holidayDetailDTO = holidayDetailDTO;
this.floatingRateDTO = floatingRateDTO;
@@ -97,6 +99,10 @@ public class ScheduleGeneratorDTO {
return this.recalculateFrom;
}
public LocalDate getRecalculateTill() {
return this.recalculateTill;
}
public Long getOverdurPenaltyWaitPeriod() {
return this.overdurPenaltyWaitPeriod;
}
@@ -2186,7 +2186,8 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
}
private LoanScheduleDTO rescheduleNextInstallments(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, Loan loan,
@Override
public LoanScheduleDTO rescheduleNextInstallments(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, Loan loan,
final HolidayDetailDTO holidayDetailDTO,
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, LocalDate rescheduleFrom,
final LocalDate scheduleTillDate) {
@@ -41,6 +41,10 @@ public interface LoanScheduleGenerator {
HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
LocalDate rescheduleFrom);
LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan,
HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
LocalDate rescheduleFrom, LocalDate rescheduleTill);
OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, LoanApplicationTerms loanApplicationTerms,
MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor);
@@ -175,6 +175,14 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
return LoanScheduleDTO.from(null, model);
}
@Override
public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicationTerms loanApplicationTerms, Loan loan,
HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor,
LocalDate rescheduleFrom, LocalDate rescheduleTill) {
return rescheduleNextInstallments(mc, loanApplicationTerms, loan, holidayDetailDTO, loanRepaymentScheduleTransactionProcessor,
rescheduleFrom);
}
@Override
public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate,
LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO,
@@ -242,11 +242,24 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
}
LocalDate recalculateFrom = null;
if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
if (loan.isInterestBearingAndInterestRecalculationEnabled() && loan.getLoanProduct().getProductInterestRecalculationDetails()
.getPreCloseInterestCalculationStrategy().calculateTillPreClosureDateEnabled()) {
recalculateFrom = transactionDate;
}
final ScheduleGeneratorDTO scheduleGeneratorDTOForPrepay = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom,
transactionDate, holidayDetailDto);
Money outstanding = loanTransactionProcessingService.fetchPrepaymentDetail(scheduleGeneratorDTOForPrepay, transactionDate, loan)
.getTotalOutstanding();
LocalDate recalculateTill = null;
if (repaymentAmount.isGreaterThanOrEqualTo(outstanding) && loan.isInterestBearingAndInterestRecalculationEnabled()
&& loan.getLoanProduct().getProductInterestRecalculationDetails().getPreCloseInterestCalculationStrategy()
.calculateTillPreClosureDateEnabled()) {
recalculateTill = transactionDate;
}
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom,
holidayDetailDto);
recalculateTill, holidayDetailDto);
if (!isHolidayValidationDone) {
final HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO();
@@ -200,7 +200,7 @@ public class LoanTransactionProcessingServiceImpl implements LoanTransactionProc
loan.getTransactionProcessingStrategyCode());
return loanScheduleGenerator.rescheduleNextInstallments(mc, loanApplicationTerms, loan, generatorDTO.getHolidayDetailDTO(),
loanRepaymentScheduleTransactionProcessor, generatorDTO.getRecalculateFrom());
loanRepaymentScheduleTransactionProcessor, generatorDTO.getRecalculateFrom(), generatorDTO.getRecalculateTill());
}
@Override
@@ -76,10 +76,16 @@ public class LoanUtilService {
public ScheduleGeneratorDTO buildScheduleGeneratorDTO(final Loan loan, final LocalDate recalculateFrom) {
final HolidayDetailDTO holidayDetailDTO = null;
return buildScheduleGeneratorDTO(loan, recalculateFrom, holidayDetailDTO);
return buildScheduleGeneratorDTO(loan, recalculateFrom, null, holidayDetailDTO);
}
public ScheduleGeneratorDTO buildScheduleGeneratorDTO(final Loan loan, final LocalDate recalculateFrom,
final LocalDate rescheduleTill) {
final HolidayDetailDTO holidayDetailDTO = null;
return buildScheduleGeneratorDTO(loan, recalculateFrom, rescheduleTill, holidayDetailDTO);
}
public ScheduleGeneratorDTO buildScheduleGeneratorDTO(final Loan loan, final LocalDate recalculateFrom, final LocalDate recalculateTill,
final HolidayDetailDTO holidayDetailDTO) {
HolidayDetailDTO holidayDetails = holidayDetailDTO;
if (holidayDetailDTO == null) {
@@ -133,7 +139,7 @@ public class LoanUtilService {
ScheduleGeneratorDTO scheduleGeneratorDTO = new ScheduleGeneratorDTO(loanScheduleFactory, applicationCurrency.toData(),
calculatedRepaymentsStartingFromDate, holidayDetails, restCalendarInstance, compoundingCalendarInstance, recalculateFrom,
overdurPenaltyWaitPeriod, floatingRateDTO, calendar, calendarHistoryDataWrapper,
recalculateTill, overdurPenaltyWaitPeriod, floatingRateDTO, calendar, calendarHistoryDataWrapper,
isInterestChargedFromDateAsDisbursementDateEnabled, numberOfDays, isSkipRepaymentOnFirstMonth,
isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled, isFirstRepaymentDateAllowedOnHoliday,
isInterestToBeRecoveredFirstWhenGreaterThanEMI, isPrincipalCompoundingDisabledForOverdueLoans);
@@ -134,4 +134,171 @@ public class LoanRepaymentTest extends BaseLoanIntegrationTest {
);
});
}
@Test
public void test_LoanRepaymentWorks_WhenOnlyOneInstallment_AndAccrualAccounting_AndMonthlyRecalculateInterest_AndMonthlyInterestCalculationPeriod_AllowPartial() {
runAt("31 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
int numberOfRepayments = 1;
int repaymentEvery = 1;
// Create Loan Product
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
.numberOfRepayments(numberOfRepayments) //
.repaymentEvery(repaymentEvery) //
.installmentAmountInMultiplesOf(null) //
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) //
.interestType(InterestType.DECLINING_BALANCE)//
.interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)//
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
.isInterestRecalculationEnabled(true)//
.recalculationRestFrequencyInterval(1)//
.recalculationRestFrequencyType(RecalculationRestFrequencyType.SAME_AS_REPAYMENT_PERIOD)//
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
.allowPartialPeriodInterestCalcualtion(true)//
.preClosureInterestCalculationStrategy(1).disallowExpectedDisbursements(false)//
.allowApprovedDisbursedAmountsOverApplied(false)//
.overAppliedNumber(null)//
.overAppliedCalculationType(null)//
.interestRatePerPeriod(10.0)//
.multiDisburseLoan(null);//
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
Long loanProductId = loanProductResponse.getResourceId();
// Apply and Approve Loan
double amount = 1000.0;
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
.repaymentEvery(repaymentEvery)//
.loanTermFrequency(numberOfRepayments)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
.interestType(InterestType.DECLINING_BALANCE)//
.interestRatePerPeriod(BigDecimal.valueOf(10.0))//
.interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD);//
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
approveLoanRequest(amount, "01 January 2023"));
Long loanId = approvedLoanResult.getLoanId();
// disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 January 2023");
// verify transactions
verifyTransactions(loanId, //
transaction(1000.0, "Disbursement", "01 January 2023")//
);
verifyPrepayAmountByRepayment(loanId, "15 January 2023");
// verify transactions
verifyTransactions(loanId, //
transaction(1000.0, "Disbursement", "01 January 2023"), //
transaction(1045.16, "Repayment", "15 January 2023"), //
transaction(45.16, "Accrual", "15 January 2023") //
);
// verify journal entries
verifyJournalEntries(loanId,
journalEntry(1000.0, loansReceivableAccount, "DEBIT"), journalEntry(1000.0, fundSource, "CREDIT"),
journalEntry(1045.16, fundSource, "DEBIT"), journalEntry(45.16, interestReceivableAccount, "CREDIT"),
journalEntry(45.16, interestReceivableAccount, "DEBIT"), journalEntry(45.16, interestIncomeAccount, "CREDIT"),
journalEntry(1000.0, fundSource, "CREDIT")
);
});
}
@Test
public void test_LoanRepaymentWorks_WhenOnlyOneInstallment_AndAccrualAccounting_AndDailyRecalculateInterest_AndMonthlyInterestCalculationPeriod_NotAllowPartial() {
runAt("31 January 2023", () -> {
// Create Client
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
int numberOfRepayments = 1;
int repaymentEvery = 1;
// Create Loan Product
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() //
.numberOfRepayments(numberOfRepayments) //
.repaymentEvery(repaymentEvery) //
.installmentAmountInMultiplesOf(null) //
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) //
.interestType(InterestType.DECLINING_BALANCE)//
.interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD)//
.interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)//
.rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)//
.isInterestRecalculationEnabled(true)//
.recalculationRestFrequencyInterval(1)//
.recalculationRestFrequencyType(RecalculationRestFrequencyType.SAME_AS_REPAYMENT_PERIOD)//
.rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)//
.allowPartialPeriodInterestCalcualtion(true)//
.preClosureInterestCalculationStrategy(2).disallowExpectedDisbursements(false)//
.allowApprovedDisbursedAmountsOverApplied(false)//
.overAppliedNumber(null)//
.overAppliedCalculationType(null)//
.interestRatePerPeriod(10.0)//
.multiDisburseLoan(null);//
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
Long loanProductId = loanProductResponse.getResourceId();
// Apply and Approve Loan
double amount = 1000.0;
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
.repaymentEvery(repaymentEvery)//
.loanTermFrequency(numberOfRepayments)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
.interestType(InterestType.DECLINING_BALANCE)//
.interestRatePerPeriod(BigDecimal.valueOf(10.0))//
.interestCalculationPeriodType(InterestCalculationPeriodType.SAME_AS_REPAYMENT_PERIOD);//
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
approveLoanRequest(amount, "01 January 2023"));
Long loanId = approvedLoanResult.getLoanId();
// disburse Loan
disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 January 2023");
// verify transactions
verifyTransactions(loanId, //
transaction(1000.0, "Disbursement", "01 January 2023")//
);
verifyPrepayAmountByRepayment(loanId, "15 January 2023");
// verify transactions
verifyTransactions(loanId, //
transaction(1000.0, "Disbursement", "01 January 2023"), //
transaction(1100.0, "Repayment", "15 January 2023"), //
transaction(100.0, "Accrual", "15 January 2023") //
);
// verify journal entries
verifyJournalEntries(loanId,
journalEntry(1000.0, loansReceivableAccount, "DEBIT"), journalEntry(1000.0, fundSource, "CREDIT"),
journalEntry(1100.0, fundSource, "DEBIT"), journalEntry(100.0, interestReceivableAccount, "CREDIT"),
journalEntry(100.0, interestReceivableAccount, "DEBIT"), journalEntry(100.0, interestIncomeAccount, "CREDIT"),
journalEntry(1000.0, fundSource, "CREDIT")
);
});
}
}