diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/AdvancePaymentsAdjustmentType.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/AdvancePaymentsAdjustmentType.java index 6b194bfcb7e..e236191a46e 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/AdvancePaymentsAdjustmentType.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/AdvancePaymentsAdjustmentType.java @@ -20,7 +20,7 @@ public enum AdvancePaymentsAdjustmentType { - RESCHEDULE_NEXT_REPAYMENTS(1), REDUCE_NUMBER_OF_INSTALLMENTS(2), REDUCE_EMI_AMOUNT(3); + RESCHEDULE_NEXT_REPAYMENTS(1), REDUCE_NUMBER_OF_INSTALLMENTS(2), REDUCE_EMI_AMOUNT(3), ADJUST_LAST_UNPAID_PERIOD(4); public final Integer value; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRescheduleStrategyMethod.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRescheduleStrategyMethod.java index fc3438911ef..293f0309a44 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRescheduleStrategyMethod.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanRescheduleStrategyMethod.java @@ -27,6 +27,7 @@ *
  • RESCHEDULE_NEXT_REPAYMENTS
  • *
  • REDUCE_NUMBER_OF_INSTALLMENTS
  • *
  • REDUCE_EMI_AMOUNT
  • + *
  • ADJUST_LAST_UNPAID_PERIOD
  • * */ @@ -35,7 +36,9 @@ public enum LoanRescheduleStrategyMethod { INVALID(0, "loanRescheduleStrategyMethod.invalid"), // RESCHEDULE_NEXT_REPAYMENTS(1, "loanRescheduleStrategyMethod.reschedule.next.repayments"), // REDUCE_NUMBER_OF_INSTALLMENTS(2, "loanRescheduleStrategyMethod.reduce.number.of.installments"), // - REDUCE_EMI_AMOUNT(3, "loanRescheduleStrategyMethod.reduce.emi.amount"); + REDUCE_EMI_AMOUNT(3, "loanRescheduleStrategyMethod.reduce.emi.amount"), // + ADJUST_LAST_UNPAID_PERIOD(4, "loanRescheduleStrategyMethod.adjust.last.unpaid.period"), // + ; private final Integer value; private final String code; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanDropdownReadPlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanDropdownReadPlatformServiceImpl.java index 5dc8f59e64a..aa3b6ac0057 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanDropdownReadPlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanDropdownReadPlatformServiceImpl.java @@ -159,7 +159,8 @@ public List retrieveRescheduleStrategyTypeOptions() { return Arrays.asList(rescheduleStrategyType(LoanRescheduleStrategyMethod.REDUCE_EMI_AMOUNT), rescheduleStrategyType(LoanRescheduleStrategyMethod.REDUCE_NUMBER_OF_INSTALLMENTS), - rescheduleStrategyType(LoanRescheduleStrategyMethod.RESCHEDULE_NEXT_REPAYMENTS)); + rescheduleStrategyType(LoanRescheduleStrategyMethod.RESCHEDULE_NEXT_REPAYMENTS), + rescheduleStrategyType(LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)); } @Override diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java index 434a219b1e7..4db9c5cdcc2 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java @@ -492,6 +492,9 @@ public static EnumOptionData rescheduleStrategyType(final LoanRescheduleStrategy case RESCHEDULE_NEXT_REPAYMENTS -> new EnumOptionData(LoanRescheduleStrategyMethod.RESCHEDULE_NEXT_REPAYMENTS.getValue().longValue(), LoanRescheduleStrategyMethod.RESCHEDULE_NEXT_REPAYMENTS.getCode(), "Reschedule next repayments"); + case ADJUST_LAST_UNPAID_PERIOD -> + new EnumOptionData(LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.getValue().longValue(), + LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.getCode(), "Adjust last, unpaid period"); default -> new EnumOptionData(LoanRescheduleStrategyMethod.INVALID.getValue().longValue(), LoanRescheduleStrategyMethod.INVALID.getCode(), "Invalid"); }; 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 30ae80d3dbe..b55f40c6834 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 @@ -61,6 +61,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductPaymentAllocationRule; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductValueConditionType; +import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod; import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes; import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; import org.apache.fineract.portfolio.loanproduct.exception.EqualAmortizationUnsupportedFeatureException; @@ -1042,7 +1043,27 @@ private void validateInterestRecalculationParams(final JsonElement element, fina final Integer rescheduleStrategyMethod = this.fromApiJsonHelper .extractIntegerNamed(LoanProductConstants.rescheduleStrategyMethodParameterName, element, Locale.getDefault()); baseDataValidator.reset().parameter(LoanProductConstants.rescheduleStrategyMethodParameterName).value(rescheduleStrategyMethod) - .notNull().inMinMaxRange(1, 3); + .notNull().inMinMaxRange(1, 4); + final LoanRescheduleStrategyMethod loanRescheduleStrategyMethod = LoanRescheduleStrategyMethod + .fromInt(rescheduleStrategyMethod); + + String loanScheduleType = LoanScheduleType.CUMULATIVE.toString(); + if (fromApiJsonHelper.parameterExists(LoanProductConstants.LOAN_SCHEDULE_TYPE, element)) { + loanScheduleType = fromApiJsonHelper.extractStringNamed(LoanProductConstants.LOAN_SCHEDULE_TYPE, element); + } + if (LoanScheduleType.CUMULATIVE.equals(LoanScheduleType.valueOf(loanScheduleType)) + && LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.equals(loanRescheduleStrategyMethod)) { + baseDataValidator.reset().parameter(LoanProductConstants.rescheduleStrategyMethodParameterName).failWithCode( + "reschedule.strategy.method.not.supported.for.loan.schedule.type.cumulative", + "Adjust last, unpaid period is only supported for Progressive loan schedule type"); + } + + if (LoanScheduleType.PROGRESSIVE.equals(LoanScheduleType.valueOf(loanScheduleType)) + && !LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.equals(loanRescheduleStrategyMethod)) { + baseDataValidator.reset().parameter(LoanProductConstants.rescheduleStrategyMethodParameterName).failWithCode( + "reschedule.strategy.method.not.supported.for.loan.schedule.type.progressive", + loanScheduleType.toString() + " reschedule strategy type is not supported for Progressive loan schedule type"); + } } RecalculationFrequencyType frequencyType = null; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/products/api/SelfLoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/products/api/SelfLoanProductsApiResource.java index 4f2d33ffa66..7569a08605a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/products/api/SelfLoanProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/products/api/SelfLoanProductsApiResource.java @@ -99,7 +99,7 @@ + "Specifies which amount portion should be added to principal for interest recalculation. \n" + "Example Values:0=NONE(Only on principal), 1=INTEREST(Principal+Interest), 2=FEE(Principal+Fee), 3=FEE And INTEREST (Principal+Fee+Interest)\n" + "rescheduleStrategyMethod\n" + "Specifies what action should perform on loan repayment schedule for advance payments. \n" - + "Example Values:1=Reschedule next repayments, 2=Reduce number of installments, 3=Reduce EMI amount\n" + + "Example Values:1=Reschedule next repayments, 2=Reduce number of installments, 3=Reduce EMI amount, 4=Adjust last, unpaid period\n" + "recalculationCompoundingFrequencyType\n" + "Specifies effective date from which the compounding of interest or fee amounts will be considered in recalculation on late payment.\n" + "Example Values:1=Same as repayment period, 2=Daily, 3=Weekly, 4=Monthly\n" + "recalculationCompoundingFrequencyInterval\n" diff --git a/fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm b/fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm index 600844d5504..ae475f2e808 100644 --- a/fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm +++ b/fineract-provider/src/main/resources/static/legacy-docs/apiLive.htm @@ -15845,7 +15845,7 @@

    Loan Products

    Specifies what action should perform on loan repayment schedule for advance payments.
    - Example Values:1=Reschedule next repayments, 2=Reduce number of installments, 3=Reduce EMI amount + Example Values:1=Reschedule next repayments, 2=Reduce number of installments, 3=Reduce EMI amount, 4=Adjust last, unpaid period recalculationCompoundingFrequencyType @@ -51748,7 +51748,7 @@

    Loan Products

    Specifies what action should perform on loan repayment schedule for advance payments.
    - Example Values:1=Reschedule next repayments, 2=Reduce number of installments, 3=Reduce EMI amount + Example Values:1=Reschedule next repayments, 2=Reduce number of installments, 3=Reduce EMI amount, 4=Adjust last, unpaid period recalculationCompoundingFrequencyType 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 9f5a5f1500a..4c83df782d0 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 @@ -44,6 +44,7 @@ import org.apache.fineract.client.models.BusinessDateRequest; import org.apache.fineract.client.models.CreditAllocationData; import org.apache.fineract.client.models.CreditAllocationOrder; +import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdLoanChargeData; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdResponse; @@ -5095,6 +5096,69 @@ public void uc147b() { }); } + // uc148a: Advanced payment allocation, with Interest Recalculation in Loan Product and Adjust last, unpaid period + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Pment. Alloc. with Interest Recalculation enabled and Adjust last, unpaid + // period + @Test + public void uc148a() { + runAt("23 March 2024", () -> { + final Integer rescheduleStrategyMethod = 4; // Adjust last, unpaid period + PostLoanProductsRequest loanProduct = createOnePeriod30DaysPeriodicAccrualProductWithAdvancedPaymentAllocationAndInterestRecalculation( + (double) 80.0, rescheduleStrategyMethod); + + final PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(loanProduct); + assertNotNull(loanProductResponse); + + final GetLoanProductsProductIdResponse loanProductDetails = loanProductHelper + .retrieveLoanProductById(loanProductResponse.getResourceId()); + assertNotNull(loanProductDetails); + assertTrue(loanProductDetails.getIsInterestRecalculationEnabled()); + assertEquals(rescheduleStrategyMethod.longValue(), + loanProductDetails.getInterestRecalculationData().getRescheduleStrategyType().getId()); + }); + } + + // uc148b: Negative Test: Advanced payment allocation, with Interest Recalculation in Loan Product but try to use + // Reduce EMI amount + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Try to Create a Loan product with Adv. Pment. Alloc. with Interest Recalculation enabled and use Reduce EMI + // amount + @Test + public void uc148b() { + runAt("23 March 2024", () -> { + final Integer rescheduleStrategyMethod = 3; // Reduce EMI amount + PostLoanProductsRequest loanProduct = createOnePeriod30DaysPeriodicAccrualProductWithAdvancedPaymentAllocationAndInterestRecalculation( + (double) 80.0, rescheduleStrategyMethod); + + CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class, + () -> loanProductHelper.createLoanProduct(loanProduct)); + + Assertions.assertTrue(callFailedRuntimeException.getMessage().contains("is not supported for Progressive loan schedule type")); + }); + } + + // uc148c: Negative Test: Comulative, with Interest Recalculation in Loan Product but try to use + // Adjust last, unpaid period + // COMULATIVE + // 1. Try to Create a Loan product with Comulative with Interest Recalculation enabled and use Adjust last, unpaid + // period + @Test + public void uc148c() { + runAt("23 March 2024", () -> { + final Integer rescheduleStrategyMethod = 4; // Adjust last, unpaid period + PostLoanProductsRequest loanProduct = createOnePeriod30DaysPeriodicAccrualProductWithAdvancedPaymentAllocationAndInterestRecalculation( + (double) 80.0, rescheduleStrategyMethod).transactionProcessingStrategyCode(LoanProductTestBuilder.DEFAULT_STRATEGY)// + .loanScheduleType(LoanScheduleType.CUMULATIVE.toString()); + + CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class, + () -> loanProductHelper.createLoanProduct(loanProduct)); + + Assertions.assertTrue(callFailedRuntimeException.getMessage() + .contains("Adjust last, unpaid period is only supported for Progressive loan schedule type")); + }); + } + private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId, Integer numberOfRepayments, String loanDisbursementDate, double amount) { LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 94cb0e7baf9..fc0e6f6e62b 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -205,8 +205,12 @@ private String getFullAdminAuthKey() { return Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey(); } - // Loan product with proper accounting setup protected PostLoanProductsRequest createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() { + return createOnePeriod30DaysPeriodicAccrualProduct((double) 0); + } + + // Loan product with proper accounting setup + protected PostLoanProductsRequest createOnePeriod30DaysPeriodicAccrualProduct(double interestRatePerPeriod) { return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6))// .shortName(Utils.uniqueRandomStringGenerator("", 4))// .description("Loan Product Description")// @@ -224,7 +228,7 @@ protected PostLoanProductsRequest createOnePeriod30DaysLongNoInterestPeriodicAcc .maxNumberOfRepayments(30)// .isLinkedToFloatingInterestRates(false)// .minInterestRatePerPeriod((double) 0)// - .interestRatePerPeriod((double) 0)// + .interestRatePerPeriod(interestRatePerPeriod)// .maxInterestRatePerPeriod((double) 100)// .interestRateFrequencyType(2)// .repaymentEvery(30)// @@ -302,6 +306,21 @@ protected PostLoanProductsRequest createOnePeriod30DaysLongNoInterestPeriodicAcc .addPaymentAllocationItem(defaultAllocation); } + protected PostLoanProductsRequest createOnePeriod30DaysPeriodicAccrualProductWithAdvancedPaymentAllocationAndInterestRecalculation( + final double interestRatePerPeriod, final Integer rescheduleStrategyMethod) { + String futureInstallmentAllocationRule = "NEXT_INSTALLMENT"; + AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(futureInstallmentAllocationRule); + + return createOnePeriod30DaysPeriodicAccrualProduct(interestRatePerPeriod) // + .transactionProcessingStrategyCode("advanced-payment-allocation-strategy")// + .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) // + .loanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.toString()) // + .addPaymentAllocationItem(defaultAllocation).enableDownPayment(false) // + .isInterestRecalculationEnabled(true).interestRecalculationCompoundingMethod(0) // + .preClosureInterestCalculationStrategy(1).recalculationRestFrequencyType(1).allowPartialPeriodInterestCalcualtion(true) // + .rescheduleStrategyMethod(rescheduleStrategyMethod); + } + private List getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) { AtomicInteger integer = new AtomicInteger(1); return Arrays.stream(paymentAllocationTypes).map(pat -> {