FINERACT-1971: Fix wrong due date calculation when loan got submitted

This commit is contained in:
Adam Saghy
2024-04-23 20:45:51 +02:00
parent 2b9e114c05
commit 039bca440b
4 changed files with 390 additions and 98 deletions
@@ -431,10 +431,12 @@ public final class LoanApplicationTerms {
this.variationsDataWrapper = new LoanTermVariationsDataWrapper(loanTermVariations);
this.actualNumberOfRepayments = numberOfRepayments + getLoanTermVariations().adjustNumberOfRepayments();
this.adjustPrincipalForFlatLoans = principal.zero();
if (this.calculatedRepaymentsStartingFromDate == null) {
this.seedDate = this.expectedDisbursementDate;
// We only change the seed date if `repaymentStartingFromDate was provided`
if (this.repaymentsStartingFromDate == null) {
this.seedDate = repaymentStartDateType.isDisbursementDate() ? expectedDisbursementDate : submittedOnDate;
} else {
this.seedDate = this.calculatedRepaymentsStartingFromDate;
// When we change the seed date we are taking the `repaymentsStartingFromDate`
this.seedDate = repaymentsStartingFromDate;
}
this.calendarHistoryDataWrapper = calendarHistoryDataWrapper;
this.isInterestChargedFromDateSameAsDisbursalDateEnabled = isInterestChargedFromDateSameAsDisbursalDateEnabled;
@@ -274,9 +274,16 @@ public class LoanScheduleAssembler {
* If user has not passed the first repayments date then then derive the same based on loan type.
*/
if (calculatedRepaymentsStartingFromDate == null) {
LocalDate tmpCalculatedRepaymentsStartingFromDate = deriveFirstRepaymentDate(loanType, repaymentEvery, expectedDisbursementDate,
repaymentPeriodFrequencyType, 0, calendar, submittedOnDate, repaymentStartDateType);
calculatedRepaymentsStartingFromDate = deriveFirstRepaymentDate(loanType, repaymentEvery, expectedDisbursementDate,
repaymentPeriodFrequencyType, loanProduct.getMinimumDaysBetweenDisbursalAndFirstRepayment(), calendar, submittedOnDate,
repaymentStartDateType);
// If calculated repayment start date does not match due to minimum days between disbursal and first
// repayment rule, we set repaymentsStartingFromDate (which will be used as seed date later)
if (!tmpCalculatedRepaymentsStartingFromDate.equals(calculatedRepaymentsStartingFromDate)) {
repaymentsStartingFromDate = calculatedRepaymentsStartingFromDate;
}
}
/*
@@ -1102,13 +1109,12 @@ public class LoanScheduleAssembler {
final RepaymentStartDateType repaymentStartDateType) {
LocalDate derivedFirstRepayment = null;
final LocalDate dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment = RepaymentStartDateType.DISBURSEMENT_DATE.equals(
repaymentStartDateType) ? expectedDisbursementDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment) : submittedOnDate;
final LocalDate dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment = expectedDisbursementDate
.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);
final LocalDate seedDate = repaymentStartDateType.isDisbursementDate() ? expectedDisbursementDate : submittedOnDate;
if (calendar != null) {
derivedFirstRepayment = deriveFirstRepaymentDateForLoans(repaymentEvery, expectedDisbursementDate, expectedDisbursementDate,
repaymentPeriodFrequencyType, minimumDaysBetweenDisbursalAndFirstRepayment, calendar, submittedOnDate,
repaymentStartDateType);
derivedFirstRepayment = deriveFirstRepaymentDateForLoans(repaymentEvery, expectedDisbursementDate, seedDate,
repaymentPeriodFrequencyType, minimumDaysBetweenDisbursalAndFirstRepayment, calendar, submittedOnDate);
} else { // Individual or group account, or JLG not linked to a meeting
LocalDate dateBasedOnRepaymentFrequency;
// Derive the first repayment date as greater date among
@@ -1116,25 +1122,13 @@ public class LoanScheduleAssembler {
// (disbursement date + minimum between disbursal and first
// repayment )
if (repaymentPeriodFrequencyType.isDaily()) {
dateBasedOnRepaymentFrequency = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
? expectedDisbursementDate.plusDays(repaymentEvery)
: submittedOnDate.plusDays(repaymentEvery);
dateBasedOnRepaymentFrequency = seedDate.plusDays(repaymentEvery);
} else if (repaymentPeriodFrequencyType.isWeekly()) {
dateBasedOnRepaymentFrequency = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
? expectedDisbursementDate.plusWeeks(repaymentEvery)
: submittedOnDate.plusWeeks(repaymentEvery);
dateBasedOnRepaymentFrequency = seedDate.plusWeeks(repaymentEvery);
} else if (repaymentPeriodFrequencyType.isMonthly()) {
dateBasedOnRepaymentFrequency = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
? expectedDisbursementDate.plusMonths(repaymentEvery)
: submittedOnDate.plusMonths(repaymentEvery);
dateBasedOnRepaymentFrequency = seedDate.plusMonths(repaymentEvery);
} else { // yearly loan
dateBasedOnRepaymentFrequency = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
? expectedDisbursementDate.plusYears(repaymentEvery)
: submittedOnDate.plusYears(repaymentEvery);
dateBasedOnRepaymentFrequency = seedDate.plusYears(repaymentEvery);
}
derivedFirstRepayment = DateUtils.isAfter(dateBasedOnRepaymentFrequency,
dateBasedOnMinimumDaysBetweenDisbursalAndFirstRepayment) ? dateBasedOnRepaymentFrequency
@@ -1146,20 +1140,16 @@ public class LoanScheduleAssembler {
private LocalDate deriveFirstRepaymentDateForLoans(final Integer repaymentEvery, final LocalDate expectedDisbursementDate,
final LocalDate refernceDateForCalculatingFirstRepaymentDate, final PeriodFrequencyType repaymentPeriodFrequencyType,
final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final Calendar calendar, final LocalDate submittedOnDate,
final RepaymentStartDateType repaymentStartDateType) {
final Integer minimumDaysBetweenDisbursalAndFirstRepayment, final Calendar calendar, final LocalDate submittedOnDate) {
boolean isMeetingSkipOnFirstDayOfMonth = configurationDomainService.isSkippingMeetingOnFirstDayOfMonthEnabled();
int numberOfDays = configurationDomainService.retreivePeroidInNumberOfDaysForSkipMeetingDate().intValue();
final String frequency = CalendarUtils.getMeetingFrequencyFromPeriodFrequencyType(repaymentPeriodFrequencyType);
final LocalDate derivedFirstRepayment = CalendarUtils.getFirstRepaymentMeetingDate(calendar,
refernceDateForCalculatingFirstRepaymentDate, repaymentEvery, frequency, isMeetingSkipOnFirstDayOfMonth, numberOfDays);
final LocalDate minimumFirstRepaymentDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(repaymentStartDateType)
? expectedDisbursementDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment)
: submittedOnDate;
final LocalDate minimumFirstRepaymentDate = expectedDisbursementDate.plusDays(minimumDaysBetweenDisbursalAndFirstRepayment);
return DateUtils.isBefore(minimumFirstRepaymentDate, derivedFirstRepayment) ? derivedFirstRepayment
: deriveFirstRepaymentDateForLoans(repaymentEvery, expectedDisbursementDate, derivedFirstRepayment,
repaymentPeriodFrequencyType, minimumDaysBetweenDisbursalAndFirstRepayment, calendar, submittedOnDate,
repaymentStartDateType);
repaymentPeriodFrequencyType, minimumDaysBetweenDisbursalAndFirstRepayment, calendar, submittedOnDate);
}
private void validateMinimumDaysBetweenDisbursalAndFirstRepayment(final LocalDate disbursalDate, final LocalDate firstRepaymentDate,
@@ -91,6 +91,7 @@ import org.apache.fineract.integrationtests.common.system.CodeHelper;
import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper;
import org.apache.fineract.integrationtests.useradministration.users.UserHelper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
@@ -102,37 +103,21 @@ import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(LoanTestLifecycleExtension.class)
public abstract class BaseLoanIntegrationTest {
protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
static {
Utils.initializeRESTAssured();
}
protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
protected final ResponseSpecification responseSpec = createResponseSpecification(Matchers.is(200));
protected final ResponseSpecification responseSpec204 = createResponseSpecification(Matchers.is(204));
protected final LoanProductHelper loanProductHelper = new LoanProductHelper();
private final String fullAdminAuthKey = getFullAdminAuthKey();
protected final RequestSpecification requestSpec = createRequestSpecification(fullAdminAuthKey);
private final String nonByPassUserAuthKey = getNonByPassUserAuthKey(requestSpec, responseSpec);
protected final AccountHelper accountHelper = new AccountHelper(requestSpec, responseSpec);
protected final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
protected final LoanProductHelper loanProductHelper = new LoanProductHelper();
protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec);
protected ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec);
protected SchedulerJobHelper schedulerJobHelper = new SchedulerJobHelper(requestSpec);
protected final InlineLoanCOBHelper inlineLoanCOBHelper = new InlineLoanCOBHelper(requestSpec, responseSpec);
protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
protected final LoanAccountLockHelper loanAccountLockHelper = new LoanAccountLockHelper(requestSpec,
createResponseSpecification(Matchers.is(202)));
protected DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
// asset
protected final Account loansReceivableAccount = accountHelper.createAssetAccount("loanPortfolio");
protected final Account interestReceivableAccount = accountHelper.createAssetAccount("interestReceivable");
protected final Account feeReceivableAccount = accountHelper.createAssetAccount("feeReceivable");
protected final Account penaltyReceivableAccount = accountHelper.createAssetAccount("penaltyReceivable");
@@ -146,7 +131,6 @@ public abstract class BaseLoanIntegrationTest {
protected final Account penaltyIncomeAccount = accountHelper.createIncomeAccount("penaltyIncome");
protected final Account feeChargeOffAccount = accountHelper.createIncomeAccount("feeChargeOff");
protected final Account penaltyChargeOffAccount = accountHelper.createIncomeAccount("penaltyChargeOff");
protected final Account recoveriesAccount = accountHelper.createIncomeAccount("recoveries");
protected final Account interestIncomeChargeOffAccount = accountHelper.createIncomeAccount("interestIncomeChargeOff");
// expense
@@ -154,6 +138,61 @@ public abstract class BaseLoanIntegrationTest {
protected final Account chargeOffFraudExpenseAccount = accountHelper.createExpenseAccount("chargeOffFraud");
protected final Account writtenOffAccount = accountHelper.createExpenseAccount();
protected final Account goodwillExpenseAccount = accountHelper.createExpenseAccount();
protected final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec);
protected ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec);
protected SchedulerJobHelper schedulerJobHelper = new SchedulerJobHelper(requestSpec);
protected final InlineLoanCOBHelper inlineLoanCOBHelper = new InlineLoanCOBHelper(requestSpec, responseSpec);
protected final LoanAccountLockHelper loanAccountLockHelper = new LoanAccountLockHelper(requestSpec,
createResponseSpecification(Matchers.is(202)));
protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
protected DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
protected static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
double principalPaid, double principalOutstanding, double paidInAdvance, double paidLate) {
GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
.filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
assertEquals(dueDate, period.getDueDate());
assertEquals(principalDue, period.getPrincipalDue());
assertEquals(principalPaid, period.getPrincipalPaid());
assertEquals(principalOutstanding, period.getPrincipalOutstanding());
assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
assertEquals(paidLate, period.getTotalPaidLateForPeriod());
}
protected static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, double principalDue,
double principalPaid, double principalOutstanding, double paidInAdvance, double paidLate) {
GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
.filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
assertEquals(principalDue, period.getPrincipalDue());
assertEquals(principalPaid, period.getPrincipalPaid());
assertEquals(principalOutstanding, period.getPrincipalOutstanding());
assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
assertEquals(paidLate, period.getTotalPaidLateForPeriod());
}
protected static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
double principalPaid, double principalOutstanding, double feeDue, double feePaid, double feeOutstanding, double penaltyDue,
double penaltyPaid, double penaltyOutstanding, double interestDue, double interestPaid, double interestOutstanding,
double paidInAdvance, double paidLate) {
GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
.filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
assertEquals(dueDate, period.getDueDate());
assertEquals(principalDue, period.getPrincipalDue());
assertEquals(principalPaid, period.getPrincipalPaid());
assertEquals(principalOutstanding, period.getPrincipalOutstanding());
assertEquals(feeDue, period.getFeeChargesDue());
assertEquals(feePaid, period.getFeeChargesPaid());
assertEquals(feeOutstanding, period.getFeeChargesOutstanding());
assertEquals(penaltyDue, period.getPenaltyChargesDue());
assertEquals(penaltyPaid, period.getPenaltyChargesPaid());
assertEquals(penaltyOutstanding, period.getPenaltyChargesOutstanding());
assertEquals(interestDue, period.getInterestDue());
assertEquals(interestPaid, period.getInterestPaid());
assertEquals(interestOutstanding, period.getInterestOutstanding());
assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
assertEquals(paidLate, period.getTotalPaidLateForPeriod());
}
private String getNonByPassUserAuthKey(RequestSpecification requestSpec, ResponseSpecification responseSpec) {
// creates the user
@@ -287,6 +326,27 @@ public abstract class BaseLoanIntegrationTest {
return advancedPaymentData;
}
protected PostLoanProductsRequest create4Period1MonthLongWithoutInterestProduct(String repaymentStrategy) {
PostLoanProductsRequest productRequest = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().multiDisburseLoan(false)//
.disallowExpectedDisbursements(false)//
.allowApprovedDisbursedAmountsOverApplied(false)//
.overAppliedCalculationType(null)//
.overAppliedNumber(null)//
.principal(1000.0)//
.numberOfRepayments(4)//
.repaymentEvery(1)//
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())//
.transactionProcessingStrategyCode(repaymentStrategy)//
;
if (AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY.equals(repaymentStrategy)) {
productRequest.loanScheduleType("PROGRESSIVE").loanScheduleProcessingType("HORIZONTAL")
.addPaymentAllocationItem(createDefaultPaymentAllocation("NEXT_INSTALLMENT"));
} else {
productRequest.loanScheduleType("CUMULATIVE").loanScheduleProcessingType(null).paymentAllocation(null);
}
return productRequest;
}
protected PostLoanProductsRequest create1InstallmentAmountInMultiplesOf4Period1MonthLongWithInterestAndAmortizationProduct(
int interestType, int amortizationType) {
return createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().multiDisburseLoan(false)//
@@ -773,52 +833,6 @@ public abstract class BaseLoanIntegrationTest {
assertEquals(totalOverpaid, loanDetails.getTotalOverpaid());
}
protected static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
double principalPaid, double principalOutstanding, double paidInAdvance, double paidLate) {
GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
.filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
assertEquals(dueDate, period.getDueDate());
assertEquals(principalDue, period.getPrincipalDue());
assertEquals(principalPaid, period.getPrincipalPaid());
assertEquals(principalOutstanding, period.getPrincipalOutstanding());
assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
assertEquals(paidLate, period.getTotalPaidLateForPeriod());
}
protected static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, double principalDue,
double principalPaid, double principalOutstanding, double paidInAdvance, double paidLate) {
GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
.filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
assertEquals(principalDue, period.getPrincipalDue());
assertEquals(principalPaid, period.getPrincipalPaid());
assertEquals(principalOutstanding, period.getPrincipalOutstanding());
assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
assertEquals(paidLate, period.getTotalPaidLateForPeriod());
}
protected static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, Integer index, LocalDate dueDate, double principalDue,
double principalPaid, double principalOutstanding, double feeDue, double feePaid, double feeOutstanding, double penaltyDue,
double penaltyPaid, double penaltyOutstanding, double interestDue, double interestPaid, double interestOutstanding,
double paidInAdvance, double paidLate) {
GetLoansLoanIdRepaymentPeriod period = loanDetails.getRepaymentSchedule().getPeriods().stream()
.filter(p -> Objects.equals(p.getPeriod(), index)).findFirst().orElseThrow();
assertEquals(dueDate, period.getDueDate());
assertEquals(principalDue, period.getPrincipalDue());
assertEquals(principalPaid, period.getPrincipalPaid());
assertEquals(principalOutstanding, period.getPrincipalOutstanding());
assertEquals(feeDue, period.getFeeChargesDue());
assertEquals(feePaid, period.getFeeChargesPaid());
assertEquals(feeOutstanding, period.getFeeChargesOutstanding());
assertEquals(penaltyDue, period.getPenaltyChargesDue());
assertEquals(penaltyPaid, period.getPenaltyChargesPaid());
assertEquals(penaltyOutstanding, period.getPenaltyChargesOutstanding());
assertEquals(interestDue, period.getInterestDue());
assertEquals(interestPaid, period.getInterestPaid());
assertEquals(interestOutstanding, period.getInterestOutstanding());
assertEquals(paidInAdvance, period.getTotalPaidInAdvanceForPeriod());
assertEquals(paidLate, period.getTotalPaidLateForPeriod());
}
protected void checkMaturityDates(long loanId, LocalDate expectedMaturityDate, LocalDate actualMaturityDate) {
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
@@ -0,0 +1,286 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.integrationtests;
import static org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.stream.Stream;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
public class LoanDueCalculationTest extends BaseLoanIntegrationTest {
private static Stream<Arguments> processingStrategy() {
return Stream.of(
Arguments.of(Named.of("originalStrategy",
DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY)), //
Arguments.of(Named.of("advancedStrategy", AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)) //
);
}
// Repayment dates are calculated from the provided date (2024-02-29). As repayment starting date was provided, it
// overrules `repayment start date type` configuration
@ParameterizedTest
@MethodSource("processingStrategy")
public void dueDateBasedOnFirstRepaymentDate(String repaymentProcessor) {
runAt("2 February 2024", () -> {
// Client and Loan account creation
final Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest loanProductsRequest = create4Period1MonthLongWithoutInterestProduct(repaymentProcessor);
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
PostLoansRequest loanRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "2024-01-31", 1000.0, 4,
(postLoansRequest) -> {
postLoansRequest.transactionProcessingStrategyCode(repaymentProcessor).repaymentEvery(1).repaymentFrequencyType(2)
.loanTermFrequency(4).loanTermFrequencyType(2).dateFormat("yyyy-MM-dd")
.repaymentsStartingFromDate(LocalDate.of(2024, 2, 29));
});
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(loanRequest);
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "31 January 2024"), //
installment(250.0, false, "29 February 2024"), //
installment(250.0, false, "29 March 2024"), //
installment(250.0, false, "29 April 2024"), //
installment(250.0, false, "29 May 2024")) //
;
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), approveLoanRequest(1000.0, "31 January 2024"));
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "31 January 2024"), //
installment(250.0, false, "29 February 2024"), //
installment(250.0, false, "29 March 2024"), //
installment(250.0, false, "29 April 2024"), //
installment(250.0, false, "29 May 2024")) //
;
disburseLoan(postLoansResponse.getLoanId(), BigDecimal.valueOf(1000.00), "31 January 2024");
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "31 January 2024"), //
installment(250.0, false, "29 February 2024"), //
installment(250.0, false, "29 March 2024"), //
installment(250.0, false, "29 April 2024"), //
installment(250.0, false, "29 May 2024")) //
;
});
}
// Repayment dates are calculated based on `repayment start date type` configuration(=Expected disbursement date).
// Expected disbursement date `2024-01-30`,
// which is used to generate repayment due date when loan got submitted and approved, however the loan got disbursed
// on `2024-01-31`,
// the repayment schedule reflects the "new date" after it got disbursed
@ParameterizedTest
@MethodSource("processingStrategy")
public void dueDateBasedOnExpectedDisbursementDate(String repaymentProcessor) {
runAt("31 March 2024", () -> {
// Client and Loan account creation
final Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest loanProductsRequest = create4Period1MonthLongWithoutInterestProduct(repaymentProcessor)
.repaymentStartDateType(RepaymentStartDateType.DISBURSEMENT_DATE.getValue());
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
PostLoansRequest loanRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "2024-01-30", 1000.0, 4,
(postLoansRequest) -> {
postLoansRequest.transactionProcessingStrategyCode(repaymentProcessor).repaymentEvery(1).repaymentFrequencyType(2)
.loanTermFrequency(4).loanTermFrequencyType(2).dateFormat("yyyy-MM-dd");
});
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(loanRequest);
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "30 January 2024"), //
installment(250.0, false, "29 February 2024"), //
installment(250.0, false, "30 March 2024"), //
installment(250.0, false, "30 April 2024"), //
installment(250.0, false, "30 May 2024")) //
;
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), approveLoanRequest(1000.0, "31 January 2024"));
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "30 January 2024"), //
installment(250.0, false, "29 February 2024"), //
installment(250.0, false, "30 March 2024"), //
installment(250.0, false, "30 April 2024"), //
installment(250.0, false, "30 May 2024")) //
;
disburseLoan(postLoansResponse.getLoanId(), BigDecimal.valueOf(1000.00), "31 March 2024");
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "31 March 2024"), //
installment(250.0, false, "30 April 2024"), //
installment(250.0, false, "31 May 2024"), //
installment(250.0, false, "30 June 2024"), //
installment(250.0, false, "31 July 2024")) //
;
});
}
// Repayment dates are calculated based on `repayment start date type` configuration(=Submitted on date). Submitted
// on date is `2024-01-31`,
// and even the expected disbursement date is `2024-02-01`, the generated repayment schedule honors the submitted on
// date
// when it got disbursed on `2024-02-03`, the repayment schedule due dates got no changed.
@ParameterizedTest
@MethodSource("processingStrategy")
public void dueDateBasedOnSubmittedOnDate(String repaymentProcessor) {
runAt("03 February 2024", () -> {
// Client and Loan account creation
final Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest loanProductsRequest = create4Period1MonthLongWithoutInterestProduct(repaymentProcessor)
.repaymentStartDateType(RepaymentStartDateType.SUBMITTED_ON_DATE.getValue());
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
PostLoansRequest loanRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "2024-02-01", 1000.0, 4,
(postLoansRequest) -> {
postLoansRequest.transactionProcessingStrategyCode(repaymentProcessor).repaymentEvery(1).repaymentFrequencyType(2)
.loanTermFrequency(4).loanTermFrequencyType(2).submittedOnDate("2024-01-31").dateFormat("yyyy-MM-dd");
});
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(loanRequest);
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "31 January 2024"), //
installment(250.0, false, "29 February 2024"), //
installment(250.0, false, "31 March 2024"), //
installment(250.0, false, "30 April 2024"), //
installment(250.0, false, "31 May 2024")) //
;
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), approveLoanRequest(1000.0, "31 January 2024"));
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "31 January 2024"), //
installment(250.0, false, "29 February 2024"), //
installment(250.0, false, "31 March 2024"), //
installment(250.0, false, "30 April 2024"), //
installment(250.0, false, "31 May 2024")) //
;
disburseLoan(postLoansResponse.getLoanId(), BigDecimal.valueOf(1000.00), "03 February 2024");
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "01 February 2024"), //
installment(250.0, false, "29 February 2024"), //
installment(250.0, false, "31 March 2024"), //
installment(250.0, false, "30 April 2024"), //
installment(250.0, false, "31 May 2024")) //
;
});
}
// Repayment dates are calculated based on `repayment start date type` configuration(=Submitted on date). Submitted
// on date is `2024-01-31 the expected disbursement date is `2024-02-26`, the minimum days between disbursement and
// first repayment is 10 days
// so the repayment schedule got amended accordingly
@ParameterizedTest
@MethodSource("processingStrategy")
public void dueDateBasedOnSubmittedOnDateButThereShallBeMinimumDaysBetweenDisbursementAndFirstRepayment(String repaymentProcessor) {
runAt("31 January 2024", () -> {
// Client and Loan account creation
final Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest loanProductsRequest = create4Period1MonthLongWithoutInterestProduct(repaymentProcessor)
.repaymentStartDateType(RepaymentStartDateType.SUBMITTED_ON_DATE.getValue())
.minimumDaysBetweenDisbursalAndFirstRepayment(10);
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
PostLoansRequest loanRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "2024-02-26", 1000.0, 4,
(postLoansRequest) -> {
postLoansRequest.transactionProcessingStrategyCode(repaymentProcessor).repaymentEvery(1).repaymentFrequencyType(2)
.loanTermFrequency(4).loanTermFrequencyType(2).submittedOnDate("2024-01-31").dateFormat("yyyy-MM-dd");
});
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(loanRequest);
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "31 January 2024"), //
installment(250.0, false, "07 March 2024"), //
installment(250.0, false, "07 April 2024"), //
installment(250.0, false, "07 May 2024"), //
installment(250.0, false, "07 June 2024")) //
;
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), approveLoanRequest(1000.0, "31 January 2024"));
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "31 January 2024"), //
installment(250.0, false, "07 March 2024"), //
installment(250.0, false, "07 April 2024"), //
installment(250.0, false, "07 May 2024"), //
installment(250.0, false, "07 June 2024")) //
;
disburseLoan(postLoansResponse.getLoanId(), BigDecimal.valueOf(1000.00), "31 January 2024");
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "31 January 2024"), //
installment(250.0, false, "07 March 2024"), //
installment(250.0, false, "07 April 2024"), //
installment(250.0, false, "07 May 2024"), //
installment(250.0, false, "07 June 2024")) //
;
});
}
// Repayment dates are calculated based on `repayment start date type` configuration(=Disbursement date). Submitted
// on date is `2024-01-31 the expected disbursement date is `2024-02-26`, the minimum days between disbursement and
// first repayment is 36 days
// so the repayment schedule got amended accordingly
@ParameterizedTest
@MethodSource("processingStrategy")
public void dueDateBasedOnExpectedDisbursalDateButThereShallBeMinimumDaysBetweenDisbursementAndFirstRepayment(
String repaymentProcessor) {
runAt("31 January 2024", () -> {
// Client and Loan account creation
final Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
PostLoanProductsRequest loanProductsRequest = create4Period1MonthLongWithoutInterestProduct(repaymentProcessor)
.repaymentStartDateType(RepaymentStartDateType.DISBURSEMENT_DATE.getValue())
.minimumDaysBetweenDisbursalAndFirstRepayment(36);
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProductsRequest);
PostLoansRequest loanRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "2024-01-31", 1000.0, 4,
(postLoansRequest) -> {
postLoansRequest.transactionProcessingStrategyCode(repaymentProcessor).repaymentEvery(1).repaymentFrequencyType(2)
.loanTermFrequency(4).loanTermFrequencyType(2).submittedOnDate("2024-01-31").dateFormat("yyyy-MM-dd");
});
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(loanRequest);
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "31 January 2024"), //
installment(250.0, false, "07 March 2024"), //
installment(250.0, false, "07 April 2024"), //
installment(250.0, false, "07 May 2024"), //
installment(250.0, false, "07 June 2024")) //
;
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), approveLoanRequest(1000.0, "31 January 2024"));
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "31 January 2024"), //
installment(250.0, false, "07 March 2024"), //
installment(250.0, false, "07 April 2024"), //
installment(250.0, false, "07 May 2024"), //
installment(250.0, false, "07 June 2024")) //
;
disburseLoan(postLoansResponse.getLoanId(), BigDecimal.valueOf(1000.00), "31 January 2024");
verifyRepaymentSchedule(postLoansResponse.getLoanId(), installment(1000.0, null, "31 January 2024"), //
installment(250.0, false, "07 March 2024"), //
installment(250.0, false, "07 April 2024"), //
installment(250.0, false, "07 May 2024"), //
installment(250.0, false, "07 June 2024")) //
;
});
}
}