This is an automated email from the ASF dual-hosted git repository. adamsaghy pushed a commit to branch release/1.13.1 in repository https://gitbox.apache.org/repos/asf/fineract.git
commit 57e1701a970712d5bca8642624a419377f23a163 Author: Jose Alberto Hernandez <[email protected]> AuthorDate: Fri Sep 26 08:16:44 2025 -0500 FINERACT-2382: Repayment schedule for Flat-Cumulative-Multi Disbursement --- .../core/data/DataValidatorBuilder.java | 2 +- .../loanschedule/domain/LoanApplicationTerms.java | 4 +- .../serialization/LoanApplicationValidator.java | 29 +-------- .../serialization/LoanProductDataValidator.java | 8 +-- ...PaymentAllocationLoanRepaymentScheduleTest.java | 69 ++++++++++++++++++++++ 5 files changed, 75 insertions(+), 37 deletions(-) diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java index c8745d7a36..26476f7c48 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java @@ -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); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java index d9c7b10587..6ef3cbe7da 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanApplicationTerms.java @@ -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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java index 6ab9f9516f..10d4c3f8dc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java @@ -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()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java index 9132f39f63..2bc26a6b7c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/serialization/LoanProductDataValidator.java @@ -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); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index 540a37267a..87ad48e08d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -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 ---------------------------------------");
