This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git

commit 2269d616dcb53d67aa945ad8b97c0ac1aa198251
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 1a8f1bedd1..3234a0bd7d 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 
---------------------------------------");

Reply via email to