FINERACT-2382: Repayment schedule for Flat-Cumulative-Multi Disbursement

This commit is contained in:
Jose Alberto Hernandez
2025-10-27 10:14:46 +01:00
committed by Adam Saghy
parent 567b12170b
commit 2269d616dc
5 changed files with 75 additions and 37 deletions
@@ -494,7 +494,7 @@ public class DataValidatorBuilder {
final Integer intValue = Integer.valueOf(this.value.toString());
if (!intValue.equals(number)) {
String validationErrorCode = "validation.msg." + this.resource + "." + this.parameter + ".not.equal.to.specified.number";
String defaultEnglishMessage = "The parameter `" + this.parameter + "` must be same as" + number;
String defaultEnglishMessage = "The parameter `" + this.parameter + "` must be same as " + number;
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage, this.parameter,
intValue, number);
this.dataValidationErrors.add(error);
@@ -1085,7 +1085,7 @@ public final class LoanApplicationTerms {
final BigDecimal loanTermFrequencyBigDecimal = calculatePeriodsInLoanTerm();
return this.annualNominalInterestRate.divide(loanTermPeriodsInYearBigDecimal, mc).divide(divisor, mc)
.multiply(loanTermFrequencyBigDecimal);
.multiply(loanTermFrequencyBigDecimal, mc);
}
private BigDecimal calculatePeriodsInLoanTerm() {
@@ -1237,7 +1237,7 @@ public final class LoanApplicationTerms {
}
if (this.installmentAmountInMultiplesOf != null) {
Money roundedPrincipalPerPeriod = Money.roundToMultiplesOf(principalPerPeriod, this.installmentAmountInMultiplesOf);
Money roundedPrincipalPerPeriod = Money.roundToMultiplesOf(principalPerPeriod, this.installmentAmountInMultiplesOf, mc);
if (interestForThisInstallment != null) {
Money roundedInterestForThisInstallment = Money.roundToMultiplesOf(interestForThisInstallment,
this.installmentAmountInMultiplesOf);
@@ -214,8 +214,6 @@ public final class LoanApplicationValidator {
expectedFirstRepaymentOnDate);
}
validateCumulativeMultiDisburse(loan);
validateLoanTermAndRepaidEveryValues(loan.getTermFrequency(), loan.getTermPeriodFrequencyType().getValue(),
loan.getLoanProductRelatedDetail().getNumberOfRepayments(), loan.getLoanProductRelatedDetail().getRepayEvery(),
loan.getLoanProductRelatedDetail().getRepaymentPeriodFrequencyType().getValue(), loan);
@@ -230,8 +228,6 @@ public final class LoanApplicationValidator {
expectedFirstRepaymentOnDate);
}
validateCumulativeMultiDisburse(loan);
validateLoanTermAndRepaidEveryValues(loan.getTermFrequency(), loan.getTermPeriodFrequencyType().getValue(),
loan.getLoanProductRelatedDetail().getNumberOfRepayments(), loan.getLoanProductRelatedDetail().getRepayEvery(),
loan.getLoanProductRelatedDetail().getRepaymentPeriodFrequencyType().getValue(), loan);
@@ -1742,16 +1738,8 @@ public final class LoanApplicationValidator {
if (transactionProcessingStrategyCode != null) {
final Integer interestType = this.fromApiJsonHelper.extractIntegerNamed(LoanApiConstants.interestTypeParameterName,
element, Locale.getDefault());
String processorCode = loanRepaymentScheduleTransactionProcessorFactory
.determineProcessor(transactionProcessingStrategyCode).getCode();
boolean isProgressive = "advanced-payment-allocation-strategy".equals(processorCode);
if (isProgressive) {
baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).value(interestType)
.ignoreIfNull().inMinMaxRange(0, 1);
} else {
baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).value(interestType)
.ignoreIfNull().integerSameAsNumber(InterestMethod.DECLINING_BALANCE.getValue());
}
baseDataValidator.reset().parameter(LoanApiConstants.interestTypeParameterName).value(interestType).ignoreIfNull()
.inMinMaxRange(0, 1);
}
} else {
if (loan.isCumulativeSchedule()) {
@@ -2208,19 +2196,6 @@ public final class LoanApplicationValidator {
}
}
private static void validateCumulativeMultiDisburse(Loan loan) {
if (loan.isCumulativeSchedule() && loan.isMultiDisburmentLoan()
&& loan.getLoanProductRelatedDetail().getInterestMethod().isFlat()) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final ApiParameterError error = ApiParameterError.generalError(
"validation.msg.loan.cumulative.multidisburse.does.not.support.flat.interest.mode",
"Cumulative multidisburse loan does NOT support FLAT interest mode.");
dataValidationErrors.add(error);
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
private Calendar getCalendarInstance(Loan loan) {
CalendarInstance calendarInstance = calendarInstanceRepository.findCalendarInstanceByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
@@ -1059,13 +1059,7 @@ public final class LoanProductDataValidator {
final Integer interestType = this.fromApiJsonHelper.extractIntegerNamed(INTEREST_TYPE, element, Locale.getDefault());
boolean isProgressive = isProgressive(element, loanProduct);
if (isProgressive) {
baseDataValidator.reset().parameter(INTEREST_TYPE).value(interestType).ignoreIfNull().inMinMaxRange(0, 1);
} else {
baseDataValidator.reset().parameter(INTEREST_TYPE).value(interestType).ignoreIfNull()
.integerSameAsNumber(InterestMethod.DECLINING_BALANCE.getValue());
}
baseDataValidator.reset().parameter(INTEREST_TYPE).value(interestType).ignoreIfNull().inMinMaxRange(0, 1);
}
final String overAppliedCalculationType = this.fromApiJsonHelper.extractStringNamed(OVER_APPLIED_CALCULATION_TYPE, element);
@@ -6251,6 +6251,75 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
});
}
// UC158: Repayment schedule handling for flat cumulative multi-disbursement
@Test
public void uc158() {
AtomicLong loanIdRef = new AtomicLong();
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
final BigDecimal principalAmount = BigDecimal.valueOf(2000.0);
runAt("1 January 2024", () -> {
// Create a Cumulative Multidisbursal and Flat Interest Type
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(
createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().interestType(InterestType.FLAT).daysInMonthType(30)//
.transactionProcessingStrategyCode(LoanProductTestBuilder.DEFAULT_STRATEGY).interestRateFrequencyType(YEARS)
.daysInYearType(365).loanScheduleType(LoanScheduleType.CUMULATIVE.toString()).repaymentEvery(1)
.installmentAmountInMultiplesOf(null)//
.repaymentFrequencyType(2L)//
);
assertNotNull(loanProductResponse.getResourceId());
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "1 January 2024",
principalAmount.doubleValue(), 3).interestCalculationPeriodType(1).interestType(InterestType.FLAT)//
.transactionProcessingStrategyCode(LoanProductTestBuilder.DEFAULT_STRATEGY).interestRateFrequencyType(YEARS)//
.interestRatePerPeriod(BigDecimal.valueOf(7.0))//
.repaymentEvery(1)//
.repaymentFrequencyType(MONTHS)//
.loanTermFrequency(3)//
.loanTermFrequencyType(MONTHS);
PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest);
loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().approvedLoanAmount(principalAmount)
.dateFormat(DATETIME_PATTERN).approvedOnDate("1 January 2024").locale("en"));
loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
new PostLoansLoanIdRequest().actualDisbursementDate("1 January 2024").dateFormat(DATETIME_PATTERN).locale("en")
.transactionAmount(BigDecimal.valueOf(1000.00)));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
validateLoanSummaryBalances(loanDetails, 1017.50, 0.00, 1000.00, 0.00, null);
validatePeriod(loanDetails, 0, LocalDate.of(2024, 1, 1), null, 1000.0, null, null, null, 0.0, 0.0, null, null, null, null, null,
null, null, null, null);
validatePeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), null, 666.67, 333.33, 0.00, 333.33, 0.0, 0.0, 0.0, 0.00, 0.00, 0.00,
5.83, 0.00, 5.83, 0.00, 0.00);
validatePeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), null, 333.34, 333.33, 0.00, 333.33, 0.0, 0.0, 0.0, 0.00, 0.00, 0.00,
5.83, 0.00, 5.83, 0.00, 0.00);
validatePeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), null, 0.00, 333.34, 0.00, 333.34, 0.0, 0.0, 0.00, 0.00, 0.00, 0.00,
5.84, 0.00, 5.84, 0.00, 0.00);
loanIdRef.set(loanResponse.getLoanId());
});
runAt("15 January 2024", () -> {
final Long loanId = loanIdRef.get();
loanTransactionHelper.disburseLoan(loanId, new PostLoansLoanIdRequest().actualDisbursementDate("15 January 2024")
.dateFormat(DATETIME_PATTERN).locale("en").transactionAmount(BigDecimal.valueOf(500.0)));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
validateLoanSummaryBalances(loanDetails, 1526.25, 0.00, 1500.00, 0.00, null);
validatePeriod(loanDetails, 0, LocalDate.of(2024, 1, 1), null, 1000.0, null, null, null, 0.0, 0.0, null, null, null, null, null,
null, null, null, null);
validatePeriod(loanDetails, 1, LocalDate.of(2024, 1, 15), null, 500.0, null, null, null, 0.0, 0.0, null, null, null, null, null,
null, null, null, null);
validatePeriod(loanDetails, 2, LocalDate.of(2024, 2, 1), null, 1000.00, 500.00, 0.00, 500.00, 0.0, 0.0, 0.0, 0.00, 0.00, 0.00,
8.75, 0.00, 8.75, 0.00, 0.00);
validatePeriod(loanDetails, 3, LocalDate.of(2024, 3, 1), null, 500.00, 500.00, 0.00, 500.00, 0.0, 0.0, 0.0, 0.00, 0.00, 0.00,
8.75, 0.00, 8.75, 0.00, 0.00);
validatePeriod(loanDetails, 4, LocalDate.of(2024, 4, 1), null, 0.00, 500.00, 0.00, 500.00, 0.0, 0.0, 0.00, 0.00, 0.00, 0.00,
8.75, 0.00, 8.75, 0.00, 0.00);
});
}
private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId,
Integer numberOfRepayments, String loanDisbursementDate, double amount) {
LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------");