diff --git a/fineract-avro-schemas/src/main/avro/generic/v1/StringEnumOptionDataV1.avsc b/fineract-avro-schemas/src/main/avro/generic/v1/StringEnumOptionDataV1.avsc new file mode 100644 index 000000000..6ab0ab1d9 --- /dev/null +++ b/fineract-avro-schemas/src/main/avro/generic/v1/StringEnumOptionDataV1.avsc @@ -0,0 +1,31 @@ +{ + "name": "StringEnumOptionDataV1", + "namespace": "org.apache.fineract.avro.generic.v1", + "type": "record", + "fields": [ + { + "default": null, + "name": "id", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "code", + "type": [ + "null", + "string" + ] + }, + { + "default": null, + "name": "value", + "type": [ + "null", + "string" + ] + } + ] +} diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc index 3fbe75e51..8966f19a0 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc @@ -720,6 +720,14 @@ "org.apache.fineract.avro.generic.v1.EnumOptionDataV1" ] }, + { + "default": null, + "name": "daysInYearCustomStrategy", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, { "default": null, "name": "isInterestRecalculationEnabled", diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanProductDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanProductDataV1.avsc index 1121e0055..331d26d4e 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanProductDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanProductDataV1.avsc @@ -419,6 +419,14 @@ "org.apache.fineract.avro.generic.v1.EnumOptionDataV1" ] }, + { + "default": null, + "name": "daysInYearCustomStrategy", + "type": [ + "null", + "org.apache.fineract.avro.generic.v1.StringEnumOptionDataV1" + ] + }, { "default": null, "name": "isInterestRecalculationEnabled", diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java index 72ad7c27d..a74064e09 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java @@ -362,16 +362,7 @@ public final class JsonCommand { } public > T enumValueOfParameterNamed(String parameterName, Class enumType) { - try { - String value = stringValueOfParameterNamedAllowingNull(parameterName); - if (value != null) { - return Enum.valueOf(enumType, value); - } else { - return null; - } - } catch (IllegalArgumentException e) { - return null; - } + return this.fromApiJsonHelper.enumValueOfParameterNamed(parameterName, this.parsedCommand, enumType); } public String stringValueOfParameterNamed(final String parameterName) { diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/FromJsonHelper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/FromJsonHelper.java index 5bd35d56d..5006bec07 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/FromJsonHelper.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/serialization/FromJsonHelper.java @@ -39,7 +39,9 @@ import java.util.Map; import java.util.Set; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; import org.springframework.context.annotation.Primary; import org.springframework.stereotype.Component; @@ -313,4 +315,18 @@ public class FromJsonHelper { return this.gsonConverter; } + public > T enumValueOfParameterNamed(String parameterName, final JsonElement element, Class enumType) { + String value = null; + try { + value = this.helperDelegator.extractStringNamed(parameterName, element, new HashSet()); + if (value != null) { + return Enum.valueOf(enumType, value); + } else { + return null; + } + } catch (IllegalArgumentException e) { + throw new PlatformApiDataValidationException(List.of(ApiParameterError.parameterError("validation.msg.enum.value.not.found", + "Enum value not exists: ", enumType.getName(), value)), e); + } + } } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInYearCustomStrategyType.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInYearCustomStrategyType.java new file mode 100644 index 000000000..97bdf8c99 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/common/domain/DaysInYearCustomStrategyType.java @@ -0,0 +1,78 @@ +/** + * 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.portfolio.common.domain; + +import java.util.Arrays; +import java.util.List; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; + +/** + * Defines the method of leap year calculation for determining the number of days in a year. + *

+ * This enum has two modes: + *

+ *
    + *
  • {@link #FULL_LEAP_YEAR} - Always considers 366 days in a leap year.
  • + *
  • {@link #FEB_29_PERIOD_ONLY} - Considers 366 days only if the period includes February 29th; otherwise, uses 365 + * days.
  • + *
+ *

+ * When using {@link DaysInYearType#ACTUAL}, the {@code FEB_29_PERIOD_ONLY} rule applies: + *

+ *
    + *
  • If the period includes February 29th, the calculation considers 366 days.
  • + *
  • Otherwise, even if the year is a leap year, the calculation uses 365 days.
  • + *
+ *

+ * Examples: + *

+ *
    + *
  • 366 Days: A period from February 10th to March 10th in a leap year will consider 366 days, since February + * 29th is within the period.
  • + *
  • 365 Days: A period from March 10th to April 10th in a leap year will consider 365 days, as February 29th + * is not within the period.
  • + *
+ */ +@Getter +@RequiredArgsConstructor +public enum DaysInYearCustomStrategyType { + + /** Always considers 366 days in a leap year. */ + FULL_LEAP_YEAR("DaysInYearCustomStrategyType.fullLeapYear", "Full Leap Year"), + + /** Considers 366 days only if the period includes February 29th; otherwise, uses 365 days. */ + FEB_29_PERIOD_ONLY("DaysInYearCustomStrategyType.feb29PeriodOnly", "Feb 29 Period Only"),; + + private final String code; + private final String humanReadableName; + + public static List getValuesAsStringEnumOptionDataList() { + return Arrays.stream(values()).map(v -> new StringEnumOptionData(v.name(), v.getCode(), v.getHumanReadableName())).toList(); + } + + public StringEnumOptionData getValueAsStringEnumOptionData() { + return new StringEnumOptionData(name(), getCode(), getHumanReadableName()); + } + + public static StringEnumOptionData getStringEnumOptionData(String name) { + return name == null || name.trim().isEmpty() ? null : DaysInYearCustomStrategyType.valueOf(name).getValueAsStringEnumOptionData(); + } +} diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java index 6df7a71fd..8e6fb3427 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/loanproduct/DefaultLoanProduct.java @@ -92,6 +92,7 @@ public enum DefaultLoanProduct implements LoanProduct { LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_CHARGEBACK_PRINCIPAL_INTEREST_FEE, // LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_CHARGEBACK_INTEREST_PENALTY_FEE_PRINCIPAL, // LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_RECALCULATION_DAILY, // + LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_LEAP_YEAR_INTEREST_RECALCULATION_DAILY, // LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_ACCRUAL_ACTIVITY, // LP2_ADV_PYMNT_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR, // LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_INTEREST_FIRST, // diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java index 36245f3f9..613428d2d 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java @@ -18,6 +18,7 @@ */ package org.apache.fineract.test.initializer.global; +import static org.apache.fineract.client.models.LoanProductRelatedDetail.DaysInYearCustomStrategyEnum.FEB_29_PERIOD_ONLY; import static org.apache.fineract.test.data.TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION; import static org.apache.fineract.test.factory.LoanProductsRequestFactory.INTEREST_CALCULATION_PERIOD_TYPE_SAME_AS_REPAYMENT; import static org.apache.fineract.test.factory.LoanProductsRequestFactory.INTEREST_RATE_FREQUENCY_TYPE_MONTH; @@ -1919,6 +1920,34 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ TestContext.INSTANCE.set( TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INT_RECALCULATION_ZERO_INT_CHARGE_OFF_INT_RECOGNITION_FROM_DISB_DATE, responseLoanProductsRequestLP2AdvPaymentInterestRecalcDailyZeroIntChargeOffIntRecognitionFromDisbDate); + + // LP2 with progressive loan schedule + horizontal + interest EMI + actual/actual + leap year + // + interest recalculation, preClosureInterestCalculationStrategy= till preclose, + // Frequency for recalculate Outstanding Principal: Daily, Frequency Interval for recalculation: 1 + // (LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_RECALCULATION_DAILY) + String name80 = DefaultLoanProduct.LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_LEAP_YEAR_INTEREST_RECALCULATION_DAILY.getName(); + PostLoanProductsRequest loanProductsRequestLP2AdvancedPaymentInterestEmiActualActualLeapYearInterestRecalculationDaily = loanProductsRequestFactory + .defaultLoanProductsRequestLP2Emi()// + .name(name80)// + .maxPrincipal(1000000.0)// + .isInterestRecalculationEnabled(true)// + .preClosureInterestCalculationStrategy(1)// + .rescheduleStrategyMethod(4)// + .interestRecalculationCompoundingMethod(0)// + .recalculationRestFrequencyType(2)// + .recalculationRestFrequencyInterval(1)// + .daysInYearType(DaysInYearType.ACTUAL.value)// + .daysInYearCustomStrategy(FEB_29_PERIOD_ONLY.getValue()).paymentAllocation(List.of(// + createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), // + createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), // + createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), // + createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));// + Response responseLoanProductsRequestLP2AdvancedPaymentInterestEmiActualActualLeapYearInterestRecalculationDaily = loanProductsApi + .createLoanProduct(loanProductsRequestLP2AdvancedPaymentInterestEmiActualActualLeapYearInterestRecalculationDaily) + .execute(); + TestContext.INSTANCE.set( + TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_LEAP_YEAR_INTEREST_RECALCULATION_DAILY, + responseLoanProductsRequestLP2AdvancedPaymentInterestEmiActualActualLeapYearInterestRecalculationDaily); } public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule, diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java index 07902adfd..1109bbe1f 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java @@ -120,6 +120,7 @@ public abstract class TestContextKey { public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_CHARGEBACK_PRINCIPAL_INTEREST_FEE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030ChargebackPrincipalInterestFee"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_CHARGEBACK_INTEREST_PENALTY_FEE_PRINCIPAL = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030ChargebackInterestPenaltyFeePrincipal"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_RECALCULATION_DAILY = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmiActualActualInterestRecalculationDaily"; + public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_LEAP_YEAR_INTEREST_RECALCULATION_DAILY = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmiActualActualLeapYearInterestRecalculationDaily"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationAccelerateMaturityChargeOffBehaviour"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_ACCRUAL_ACTIVITY = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030AccrualActivity"; public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_RECOGNITION_DISBURSEMENT_DAILY_EMI_360_30_ACCRUAL_ACTIVITY = "loanProductCreateResponseLP2AdvancedPaymentInterestRecognitionDisbursementDailyEmi36030AccrualActivity"; diff --git a/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature b/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature index 4d93f9638..898e11ca8 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/EMICalculation.feature @@ -7728,3 +7728,111 @@ Feature: EMI calculation and repayment schedule checks for interest bearing loan | 10 February 2025 | Repayment | 900.0 | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | false | false | Then Loan status will be "CLOSED_OBLIGATIONS_MET" Then Loan closedon_date is "10 February 2025" + + @TestRailId:C3538 + Scenario: Verify leap year calculation with Feb month split between periods - UC1 + When Admin sets the business date to "12 December 2023" + When Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_LEAP_YEAR_INTEREST_RECALCULATION_DAILY | 12 December 2023 | 10000 | 9.482 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "12 December 2023" with "10000" amount and expected disbursement date on "12 December 2023" + When Admin successfully disburse the loan on "12 December 2023" with "10000" EUR transaction amount + Then Loan Repayment schedule has 6 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 12 December 2023 | | 10000.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 12 January 2024 | | 8367.32 | 1632.68 | 80.53 | 0.0 | 0.0 | 1713.21 | 0.0 | 0.0 | 0.0 | 1713.21 | + | 2 | 31 | 12 February 2024 | | 6721.49 | 1645.83 | 67.38 | 0.0 | 0.0 | 1713.21 | 0.0 | 0.0 | 0.0 | 1713.21 | + | 3 | 29 | 12 March 2024 | | 5058.78 | 1662.71 | 50.5 | 0.0 | 0.0 | 1713.21 | 0.0 | 0.0 | 0.0 | 1713.21 | + | 4 | 31 | 12 April 2024 | | 3386.31 | 1672.47 | 40.74 | 0.0 | 0.0 | 1713.21 | 0.0 | 0.0 | 0.0 | 1713.21 | + | 5 | 30 | 12 May 2024 | | 1699.49 | 1686.82 | 26.39 | 0.0 | 0.0 | 1713.21 | 0.0 | 0.0 | 0.0 | 1713.21 | + | 6 | 31 | 12 June 2024 | | 0.0 | 1699.49 | 13.69 | 0.0 | 0.0 | 1713.18 | 0.0 | 0.0 | 0.0 | 1713.18 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 10000.0 | 279.23 | 0.0 | 0.0 | 10279.23 | 0.0 | 0.0 | 0.0 | 10279.23 | + + @TestRailId:C3539 + Scenario: Verify leap year calculation with no February month but leap year - UC2 + When Admin sets the business date to "23 July 2024" + When Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_LEAP_YEAR_INTEREST_RECALCULATION_DAILY | 23 July 2024 | 15000 | 12 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 4 | MONTHS | 1 | MONTHS | 4 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "23 July 2024" with "15000" amount and expected disbursement date on "23 July 2024" + When Admin successfully disburse the loan on "23 July 2024" with "15000" EUR transaction amount + Then Loan Repayment schedule has 4 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 23 July 2024 | | 15000.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 31 | 23 August 2024 | | 11307.47 | 3692.53 | 152.88 | 0.0 | 0.0 | 3845.41 | 0.0 | 0.0 | 0.0 | 3845.41 | + | 2 | 31 | 23 September 2024| | 7577.3 | 3730.17 | 115.24 | 0.0 | 0.0 | 3845.41 | 0.0 | 0.0 | 0.0 | 3845.41 | + | 3 | 30 | 23 October 2024 | | 3806.63 | 3770.67 | 74.74 | 0.0 | 0.0 | 3845.41 | 0.0 | 0.0 | 0.0 | 3845.41 | + | 4 | 31 | 23 November 2024 | | 0.0 | 3806.63 | 38.8 | 0.0 | 0.0 | 3845.43 | 0.0 | 0.0 | 0.0 | 3845.43 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 15000.0 | 381.66 | 0.0 | 0.0 | 15381.66 | 0.0 | 0.0 | 0.0 | 15381.66 | + + @TestRailId:C3540 + Scenario: Verify leap year calculation with February in one period - UC3 + When Admin sets the business date to "31 October 2023" + When Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_LEAP_YEAR_INTEREST_RECALCULATION_DAILY | 31 October 2023 | 245000 | 45 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "31 October 2023" with "245000" amount and expected disbursement date on "31 October 2023" + When Admin successfully disburse the loan on "31 October 2023" with "245000" EUR transaction amount + Then Loan Repayment schedule has 6 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 31 October 2023 | | 245000.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 30 | 30 November 2023 | | 207713.25 | 37286.75 | 9061.64 | 0.0 | 0.0 | 46348.39 | 0.0 | 0.0 | 0.0 | 46348.39 | + | 2 | 31 | 31 December 2023 | | 169303.49 | 38409.76 | 7938.63 | 0.0 | 0.0 | 46348.39 | 0.0 | 0.0 | 0.0 | 46348.39 | + | 3 | 31 | 31 January 2024 | | 129425.74 | 39877.75 | 6470.64 | 0.0 | 0.0 | 46348.39 | 0.0 | 0.0 | 0.0 | 46348.39 | + | 4 | 29 | 29 February 2024 | | 87692.12 | 41733.62 | 4614.77 | 0.0 | 0.0 | 46348.39 | 0.0 | 0.0 | 0.0 | 46348.39 | + | 5 | 31 | 31 March 2024 | | 44695.25 | 42996.87 | 3351.52 | 0.0 | 0.0 | 46348.39 | 0.0 | 0.0 | 0.0 | 46348.39 | + | 6 | 30 | 30 April 2024 | | 0.0 | 44695.25 | 1653.11 | 0.0 | 0.0 | 46348.36 | 0.0 | 0.0 | 0.0 | 46348.36 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 245000.0 | 33090.31 | 0.0 | 0.0 | 278090.31 | 0.0 | 0.0 | 0.0 | 278090.31 | + + @TestRailId:C3541 + Scenario: Verify leap year calculation with no February month - leap and non-leap year split - UC4 + When Admin sets the business date to "31 October 2024" + When Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_LEAP_YEAR_INTEREST_RECALCULATION_DAILY | 31 October 2024 | 2450 | 9.99 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 6 | MONTHS | 1 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "31 October 2024" with "2450" amount and expected disbursement date on "31 October 2024" + When Admin successfully disburse the loan on "31 October 2024" with "2450" EUR transaction amount + Then Loan Repayment schedule has 6 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 31 October 2024 | | 2450.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 30 | 30 November 2024 | | 2049.88 | 400.12 | 20.12 | 0.0 | 0.0 | 420.24 | 0.0 | 0.0 | 0.0 | 420.24 | + | 2 | 31 | 31 December 2024 | | 1647.03 | 402.85 | 17.39 | 0.0 | 0.0 | 420.24 | 0.0 | 0.0 | 0.0 | 420.24 | + | 3 | 31 | 31 January 2025 | | 1240.76 | 406.27 | 13.97 | 0.0 | 0.0 | 420.24 | 0.0 | 0.0 | 0.0 | 420.24 | + | 4 | 28 | 28 February 2025 | | 830.03 | 410.73 | 9.51 | 0.0 | 0.0 | 420.24 | 0.0 | 0.0 | 0.0 | 420.24 | + | 5 | 31 | 31 March 2025 | | 416.83 | 413.2 | 7.04 | 0.0 | 0.0 | 420.24 | 0.0 | 0.0 | 0.0 | 420.24 | + | 6 | 30 | 30 April 2025 | | 0.0 | 416.83 | 3.42 | 0.0 | 0.0 | 420.25 | 0.0 | 0.0 | 0.0 | 420.25 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 2450.0 | 71.45 | 0.0 | 0.0 | 2521.45 | 0.0 | 0.0 | 0.0 | 2521.45 | + + @TestRailId:C3542 + Scenario: Verify leap year calculation with no leap year - UC5 + When Admin sets the business date to "29 October 2022" + When Admin creates a client with random data + When Admin creates a fully customized loan with the following data: + | LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy | + | LP2_ADV_PYMNT_INTEREST_DAILY_EMI_ACTUAL_ACTUAL_INTEREST_RECALCULATION_DAILY | 29 October 2022 | 5000 | 7 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 12 | MONTHS | 2 | MONTHS | 6 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION | + And Admin successfully approves the loan on "29 October 2022" with "5000" amount and expected disbursement date on "29 October 2022" + When Admin successfully disburse the loan on "29 October 2022" with "5000" EUR transaction amount + Then Loan Repayment schedule has 6 periods, with the following data for periods: + | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | | | 29 October 2022 | | 5000.0 | | | 0.0 | | 0.0 | 0.0 | | | | + | 1 | 61 | 29 December 2022 | | 4190.81 | 809.19 | 58.49 | 0.0 | 0.0 | 867.68 | 0.0 | 0.0 | 0.0 | 867.68 | + | 2 | 61 | 28 February 2023 | | 3372.16 | 818.65 | 49.03 | 0.0 | 0.0 | 867.68 | 0.0 | 0.0 | 0.0 | 867.68 | + | 3 | 60 | 29 April 2023 | | 2543.28 | 828.88 | 38.8 | 0.0 | 0.0 | 867.68 | 0.0 | 0.0 | 0.0 | 867.68 | + | 4 | 61 | 29 June 2023 | | 1705.35 | 837.93 | 29.75 | 0.0 | 0.0 | 867.68 | 0.0 | 0.0 | 0.0 | 867.68 | + | 5 | 61 | 29 August 2023 | | 857.62 | 847.73 | 19.95 | 0.0 | 0.0 | 867.68 | 0.0 | 0.0 | 0.0 | 867.68 | + | 6 | 61 | 29 October 2023 | | 0.0 | 857.62 | 10.03 | 0.0 | 0.0 | 867.65 | 0.0 | 0.0 | 0.0 | 867.65 | + Then Loan Repayment schedule has the following data in Total row: + | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | + | 5000.0 | 206.05 | 0.0 | 0.0 | 5206.05 | 0.0 | 0.0 | 0.0 | 5206.05 | \ No newline at end of file diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature index f3481a96c..a174cb11c 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanAccrualTransaction.feature @@ -1139,7 +1139,7 @@ Feature: LoanAccrualTransaction | 20 February 2025 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | | 21 February 2025 | Accrual | 0.02 | 0.0 | 0.02 | 0.0 | 0.0 | 0.0 | false | false | - @TestRailId:C3532 + @TestRailId:C3535 Scenario: Verify the accrual activity creation in case of full repayment on maturity date - absence of negative numbers with low interest rate and small principal When Admin sets the business date to "19 February 2025" And Admin creates a client with random data @@ -1280,7 +1280,7 @@ Feature: LoanAccrualTransaction | 19 February 2025 | Repayment | 0.67 | 0.6 | 0.07 | 0.0 | 0.0 | 6.91 | false | false | | 20 February 2025 | Merchant Issued Refund | 3.35 | 3.35 | 0.0 | 0.0 | 0.0 | 3.56 | false | false | - @TestRailId:C3533 + @TestRailId:C3536 Scenario: Verify the accrual activity creation in case of full repayment on maturity date - absence of negative numbers with high interest rate and large principal When Admin sets the business date to "19 February 2025" And Admin creates a client with random data @@ -1424,7 +1424,7 @@ Feature: LoanAccrualTransaction | 20 February 2025 | Accrual | 12.33 | 0.0 | 12.33 | 0.0 | 0.0 | 0.0 | false | false | | 20 February 2025 | Accrual | 12.33 | 0.0 | 12.33 | 0.0 | 0.0 | 0.0 | false | false | - @TestRailId:C3534 + @TestRailId:C3537 Scenario: Verify the accrual activity creation in case of full repayment on maturity date - absence of negative numbers with mid-range interest rate and mid-range principal When Admin sets the business date to "19 February 2025" And Admin creates a client with random data diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java index 1d9dce39d..00506db2c 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java @@ -93,6 +93,7 @@ public interface LoanApiConstants { String linkAccountIdParameterName = "linkAccountId"; String createStandingInstructionAtDisbursementParameterName = "createStandingInstructionAtDisbursement"; String daysInYearTypeParameterName = "daysInYearType"; + String daysInYearCustomStrategyParameterName = "daysInYearCustomStrategy"; String daysInMonthTypeParameterName = "daysInMonthType"; String MULTIDISBURSE_DETAILS_PARAMNAME = "multiDisburseDetails"; 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 2cb295986..0dcc26248 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 @@ -38,6 +38,7 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarInstance; import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.common.domain.DayOfWeekType; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.NthDayType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; @@ -230,6 +231,7 @@ public final class LoanApplicationTerms { private List supportedInterestRefundTypes; private LoanChargeOffBehaviour chargeOffBehaviour; private boolean interestRecognitionOnDisbursementDate; + private DaysInYearCustomStrategyType daysInYearCustomStrategy; private LoanApplicationTerms(Builder builder) { this.currency = builder.currency; @@ -294,6 +296,7 @@ public final class LoanApplicationTerms { private LocalDate seedDate; private MathContext mc; private Boolean interestRecognitionOnDisbursementDate; + private DaysInYearCustomStrategyType daysInYearCustomStrategy; public Builder currency(CurrencyData currency) { this.currency = currency; @@ -413,6 +416,11 @@ public final class LoanApplicationTerms { public LoanApplicationTerms build() { return new LoanApplicationTerms(this); } + + public Builder daysInYearCustomStrategy(DaysInYearCustomStrategyType daysInYearCustomStrategy) { + this.daysInYearCustomStrategy = daysInYearCustomStrategy; + return this; + } } public static LoanApplicationTerms assembleFrom(LoanRepaymentScheduleModelData modelData, MathContext mc) { @@ -439,7 +447,8 @@ public final class LoanApplicationTerms { .inArrearsTolerance(Money.zero(modelData.currency(), mc)).disbursementDatas(new ArrayList<>()) .isDownPaymentEnabled(modelData.downPaymentEnabled()).downPaymentPercentage(downPaymentPercentage) .submittedOnDate(modelData.scheduleGenerationStartDate()).seedDate(seedDate) - .interestRecognitionOnDisbursementDate(modelData.interestRecognitionOnDisbursementDate()).mc(mc).build(); + .interestRecognitionOnDisbursementDate(modelData.interestRecognitionOnDisbursementDate()) + .daysInYearCustomStrategy(modelData.daysInYearCustomStrategy()).mc(mc).build(); } public static LoanApplicationTerms assembleFrom(final CurrencyData currency, final Integer loanTermFrequency, @@ -470,7 +479,8 @@ public final class LoanApplicationTerms { final LocalDate submittedOnDate, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes, - final LoanChargeOffBehaviour chargeOffBehaviour, final boolean interestRecognitionOnDisbursementDate) { + final LoanChargeOffBehaviour chargeOffBehaviour, final boolean interestRecognitionOnDisbursementDate, + final DaysInYearCustomStrategyType daysInYearCustomStrategy) { final LoanRescheduleStrategyMethod rescheduleStrategyMethod = null; final CalendarHistoryDataWrapper calendarHistoryDataWrapper = null; @@ -490,7 +500,7 @@ public final class LoanApplicationTerms { isPrincipalCompoundingDisabledForOverdueLoans, enableDownPayment, disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, - interestRecognitionOnDisbursementDate); + interestRecognitionOnDisbursementDate, daysInYearCustomStrategy); } @@ -564,7 +574,7 @@ public final class LoanApplicationTerms { disbursedAmountPercentageForDownPayment, isAutoRepaymentForDownPaymentEnabled, repaymentStartDateType, submittedOnDate, loanScheduleType, loanScheduleProcessingType, fixedLength, loanProductRelatedDetail.isEnableAccrualActivityPosting(), loanProductRelatedDetail.getSupportedInterestRefundTypes(), loanProductRelatedDetail.getChargeOffBehaviour(), - loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()); + loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate(), loanProductRelatedDetail.getDaysInYearCustomStrategy()); } private LoanApplicationTerms(final CurrencyData currency, final Integer loanTermFrequency, @@ -595,7 +605,7 @@ public final class LoanApplicationTerms { final RepaymentStartDateType repaymentStartDateType, final LocalDate submittedOnDate, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes, final LoanChargeOffBehaviour chargeOffBehaviour, - final boolean interestRecognitionOnDisbursementDate) { + final boolean interestRecognitionOnDisbursementDate, final DaysInYearCustomStrategyType daysInYearCustomStrategy) { this.currency = currency; this.loanTermFrequency = loanTermFrequency; @@ -696,6 +706,7 @@ public final class LoanApplicationTerms { this.supportedInterestRefundTypes = supportedInterestRefundTypes; this.chargeOffBehaviour = chargeOffBehaviour; this.interestRecognitionOnDisbursementDate = interestRecognitionOnDisbursementDate; + this.daysInYearCustomStrategy = daysInYearCustomStrategy; } public Money adjustPrincipalIfLastRepaymentPeriod(final Money principalForPeriod, final Money totalCumulativePrincipalToDate, @@ -1557,7 +1568,7 @@ public final class LoanApplicationTerms { this.interestRecalculationEnabled, this.isEqualAmortization, this.isDownPaymentEnabled, this.disbursedAmountPercentageForDownPayment, this.isAutoRepaymentForDownPaymentEnabled, this.loanScheduleType, this.loanScheduleProcessingType, this.fixedLength, this.enableAccrualActivityPosting, this.supportedInterestRefundTypes, - this.chargeOffBehaviour, this.interestRecognitionOnDisbursementDate); + this.chargeOffBehaviour, this.interestRecognitionOnDisbursementDate, this.daysInYearCustomStrategy); } public LoanProductMinimumRepaymentScheduleRelatedDetail toLoanProductRelatedDetailMinimumData() { @@ -1567,7 +1578,8 @@ public final class LoanApplicationTerms { interestPaymentGrace, principalGrace, recurringMoratoriumOnPrincipalPeriods, interestMethod, interestCalculationPeriodMethod, daysInYearType, daysInMonthType, amortizationMethod, repaymentPeriodFrequencyType, repaymentEvery, numberOfRepayments, - isInterestChargedFromDateSameAsDisbursalDateEnabled != null && isInterestChargedFromDateSameAsDisbursalDateEnabled); + isInterestChargedFromDateSameAsDisbursalDateEnabled != null && isInterestChargedFromDateSameAsDisbursalDateEnabled, + daysInYearCustomStrategy); } public Integer getLoanTermFrequency() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java index ec6f11b6d..286f4d8a0 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanRepaymentScheduleModelData.java @@ -18,12 +18,14 @@ */ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; +import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.time.LocalDate; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; public record LoanRepaymentScheduleModelData(@NotNull LocalDate scheduleGenerationStartDate, @NotNull CurrencyData currency, @@ -31,15 +33,5 @@ public record LoanRepaymentScheduleModelData(@NotNull LocalDate scheduleGenerati @NotNull int repaymentFrequency, @NotBlank String repaymentFrequencyType, @NotNull BigDecimal annualNominalInterestRate, @NotNull boolean downPaymentEnabled, @NotNull DaysInMonthType daysInMonth, @NotNull DaysInYearType daysInYear, BigDecimal downPaymentPercentage, Integer installmentAmountInMultiplesOf, Integer fixedLength, - @NotNull Boolean interestRecognitionOnDisbursementDate) { - - LoanRepaymentScheduleModelData(@NotNull LocalDate scheduleGenerationStartDate, @NotNull CurrencyData currency, - @NotNull BigDecimal disbursementAmount, @NotNull LocalDate disbursementDate, @NotNull int numberOfRepayments, - @NotNull int repaymentFrequency, @NotBlank String repaymentFrequencyType, @NotNull BigDecimal annualNominalInterestRate, - @NotNull boolean downPaymentEnabled, @NotNull DaysInMonthType daysInMonth, @NotNull DaysInYearType daysInYear, - BigDecimal downPaymentPercentage, Integer installmentAmountInMultiplesOf, Integer fixedLength) { - this(scheduleGenerationStartDate, currency, disbursementAmount, disbursementDate, numberOfRepayments, repaymentFrequency, - repaymentFrequencyType, annualNominalInterestRate, downPaymentEnabled, daysInMonth, daysInYear, downPaymentPercentage, - installmentAmountInMultiplesOf, fixedLength, false); - } + @NotNull Boolean interestRecognitionOnDisbursementDate, @Nullable DaysInYearCustomStrategyType daysInYearCustomStrategy) { } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java index 5c117d672..7ee57241e 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/LoanProductConstants.java @@ -81,6 +81,7 @@ public interface LoanProductConstants { // Interest recalculation related String IS_INTEREST_RECALCULATION_ENABLED_PARAMETER_NAME = "isInterestRecalculationEnabled"; String DAYS_IN_YEAR_TYPE_PARAMETER_NAME = "daysInYearType"; + String DAYS_IN_YEAR_CUSTOM_STRATEGY_TYPE_PARAMETER_NAME = "daysInYearCustomStrategy"; String DAYS_IN_MONTH_TYPE_PARAMETER_NAME = "daysInMonthType"; String interestRecalculationCompoundingMethodParameterName = "interestRecalculationCompoundingMethod"; String rescheduleStrategyMethodParameterName = "rescheduleStrategyMethod"; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java index b5488dddc..070a830ff 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResourceSwagger.java @@ -134,6 +134,8 @@ final class LoanProductsApiResourceSwagger { public Integer daysInMonthType; @Schema(example = "1") public Integer daysInYearType; + @Schema(example = "FULL_LEAP_YEAR") + public String daysInYearCustomStrategy; @Schema(example = "true") public Boolean allowPartialPeriodInterestCalcualtion; @Schema(example = "179") @@ -457,6 +459,18 @@ final class LoanProductsApiResourceSwagger { public String description; } + static final class GetLoanProductsDaysInYearCustomStrategy { + + private GetLoanProductsDaysInYearCustomStrategy() {} + + @Schema(example = "FULL_LEAP_YEAR") + public String id; + @Schema(example = "DaysInYearCustomStrategyType.fullLeapYear") + public String code; + @Schema(example = "Full Leap Year") + public String description; + } + static final class GetLoanProductsRepaymentStartDateType { private GetLoanProductsRepaymentStartDateType() {} @@ -614,6 +628,7 @@ final class LoanProductsApiResourceSwagger { public List numberOfRepaymentVariationsForBorrowerCycle; public GetLoansProductsDaysInMonthType daysInMonthType; public GetLoansProductsDaysInYearType daysInYearType; + public GetLoanProductsDaysInYearCustomStrategy daysInYearCustomStrategy; @Schema(example = "true") public Boolean isInterestRecalculationEnabled; public GetLoanProductsInterestRecalculationData interestRecalculationData; @@ -722,6 +737,18 @@ final class LoanProductsApiResourceSwagger { public String description; } + static final class GetLoanProductsDaysInYearCustomStrategyTemplateType { + + private GetLoanProductsDaysInYearCustomStrategyTemplateType() {} + + @Schema(example = "FULL_LEAP_YEAR") + public String id; + @Schema(example = "DaysInYearCustomStrategyType.fullLeapYear") + public String code; + @Schema(example = "Full Leap Year") + public String description; + } + static final class GetLoanProductsInterestRecalculationTemplateData { private GetLoanProductsInterestRecalculationTemplateData() {} @@ -1073,6 +1100,7 @@ final class LoanProductsApiResourceSwagger { public GetLoanProductsTemplateResponse.GetLoanProductsAccountingRule accountingRule; public GetLoansProductsDaysInMonthTemplateType daysInMonthType; public GetLoanProductsDaysInYearTemplateType daysInYearType; + public GetLoanProductsDaysInYearCustomStrategyTemplateType daysInYearCustomStrategy; @Schema(example = "false") public Boolean isInterestRecalculationEnabled; public GetLoanProductsInterestRecalculationTemplateData interestRecalculationData; @@ -1091,6 +1119,7 @@ final class LoanProductsApiResourceSwagger { public Set valueConditionTypeOptions; public Set daysInMonthTypeOptions; public Set daysInYearTypeOptions; + public Set daysInYearTypeCustomStrategyOptions; public Set interestRecalculationCompoundingTypeOptions; public Set rescheduleStrategyTypeOptions; public Set interestRecalculationFrequencyTypeOptions; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java index fc36e70aa..bd579d984 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductData.java @@ -40,6 +40,7 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.calendar.data.CalendarData; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.common.service.CommonEnumerations; @@ -235,6 +236,8 @@ public class LoanProductData implements Serializable { private final EnumOptionData loanScheduleType; private final EnumOptionData loanScheduleProcessingType; private final boolean interestRecognitionOnDisbursementDate; + private final List daysInYearCustomStrategyOptions; + private final StringEnumOptionData daysInYearCustomStrategy; /** * Used when returning lookup information about loan product for dropdowns. @@ -334,12 +337,11 @@ public class LoanProductData implements Serializable { final boolean enableInstallmentLevelDelinquency = false; final EnumOptionData loanScheduleType = null; final EnumOptionData loanScheduleProcessingType = null; - final EnumOptionData loanScheduleTypeOptions = null; - final EnumOptionData loanScheduleProcessingTypeOptions = null; final boolean enableAccrualActivityPosting = false; final List supportedInterestRefundTypes = null; final StringEnumOptionData chargeOffBehaviour = null; final boolean interestRecognitionOnDisbursementDate = false; + final StringEnumOptionData daysInYearTypeCustomStrategy = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -361,7 +363,7 @@ public class LoanProductData implements Serializable { overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, - interestRecognitionOnDisbursementDate); + interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy); } @@ -465,6 +467,7 @@ public class LoanProductData implements Serializable { final List supportedInterestRefundTypes = null; final StringEnumOptionData chargeOffBehaviour = null; final boolean interestRecognitionOnDisbursementDate = false; + final StringEnumOptionData daysInYearTypeCustomStrategy = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -486,7 +489,7 @@ public class LoanProductData implements Serializable { overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, - interestRecognitionOnDisbursementDate); + interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy); } @@ -597,6 +600,7 @@ public class LoanProductData implements Serializable { final List supportedInterestRefundTypes = null; final StringEnumOptionData chargeOffBehaviour = LoanChargeOffBehaviour.REGULAR.getValueAsStringEnumOptionData(); final boolean interestRecognitionOnDisbursementDate = false; + final StringEnumOptionData daysInYearTypeCustomStrategy = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -618,7 +622,7 @@ public class LoanProductData implements Serializable { overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocation, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, - interestRecognitionOnDisbursementDate); + interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy); } @@ -723,6 +727,7 @@ public class LoanProductData implements Serializable { final List supportedInterestRefundTypes = null; final StringEnumOptionData chargeOffBehaviour = null; final boolean interestRecognitionOnDisbursementDate = false; + final StringEnumOptionData daysInYearTypeCustomStrategy = null; return new LoanProductData(id, name, shortName, description, currency, principal, minPrincipal, maxPrincipal, tolerance, numberOfRepayments, minNumberOfRepayments, maxNumberOfRepayments, repaymentEvery, interestRatePerPeriod, @@ -744,7 +749,7 @@ public class LoanProductData implements Serializable { overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, paymentAllocation, creditAllocationData, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, - interestRecognitionOnDisbursementDate); + interestRecognitionOnDisbursementDate, daysInYearTypeCustomStrategy); } public static LoanProductData withAccountingDetails(final LoanProductData productData, final Map accountingMappings, @@ -798,7 +803,8 @@ public class LoanProductData implements Serializable { final EnumOptionData repaymentStartDateType, final boolean enableInstallmentLevelDelinquency, final EnumOptionData loanScheduleType, final EnumOptionData loanScheduleProcessingType, final Integer fixedLength, final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes, - StringEnumOptionData chargeOffBehaviour, final boolean interestRecognitionOnDisbursementDate) { + StringEnumOptionData chargeOffBehaviour, final boolean interestRecognitionOnDisbursementDate, + final StringEnumOptionData daysInYearCustomStrategy) { this.id = id; this.name = name; this.shortName = shortName; @@ -854,6 +860,7 @@ public class LoanProductData implements Serializable { this.rateOptions = rateOptions; this.rates = rates; this.isRatesEnabled = isRatesEnabled; + this.daysInYearCustomStrategy = daysInYearCustomStrategy; this.chargeOptions = null; this.penaltyOptions = null; @@ -942,6 +949,7 @@ public class LoanProductData implements Serializable { this.chargeOffBehaviourOptions = null; this.chargeOffReasonOptions = null; this.interestRecognitionOnDisbursementDate = interestRecognitionOnDisbursementDate; + this.daysInYearCustomStrategyOptions = DaysInYearCustomStrategyType.getValuesAsStringEnumOptionDataList(); } public LoanProductData(final LoanProductData productData, final Collection chargeOptions, @@ -965,7 +973,8 @@ public class LoanProductData implements Serializable { final List loanScheduleProcessingTypeOptions, final List creditAllocationTransactionTypes, final List creditAllocationAllocationTypes, final List supportedInterestRefundTypesOptions, - final List chargeOffBehaviourOptions, final List chargeOffReasonOptions) { + final List chargeOffBehaviourOptions, final List chargeOffReasonOptions, + final List daysInYearCustomStrategyOptions) { this.id = productData.id; this.name = productData.name; @@ -1127,6 +1136,8 @@ public class LoanProductData implements Serializable { this.chargeOffBehaviourOptions = chargeOffBehaviourOptions; this.chargeOffReasonOptions = chargeOffReasonOptions; this.interestRecognitionOnDisbursementDate = productData.interestRecognitionOnDisbursementDate; + this.daysInYearCustomStrategyOptions = daysInYearCustomStrategyOptions; + this.daysInYearCustomStrategy = productData.daysInYearCustomStrategy; } private Collection nullIfEmpty(final Collection charges) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductRelatedDetailMinimumData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductRelatedDetailMinimumData.java index 3ff587d6c..ffdfc47d7 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductRelatedDetailMinimumData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/data/LoanProductRelatedDetailMinimumData.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanproduct.data; import java.math.BigDecimal; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; @@ -46,13 +47,15 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe private final Integer repaymentEvery; private final Integer numberOfRepayments; private final boolean interestRecognitionOnDisbursementDate; + private final DaysInYearCustomStrategyType daysInYearCustomStrategy; public LoanProductRelatedDetailMinimumData(CurrencyData currency, BigDecimal interestRatePerPeriod, BigDecimal annualNominalInterestRate, Integer interestChargingGrace, Integer interestPaymentGrace, Integer principalGrace, Integer recurringMoratoriumOnPrincipalPeriods, InterestMethod interestMethod, InterestCalculationPeriodMethod interestCalculationPeriodMethod, DaysInYearType daysInYearType, DaysInMonthType daysInMonthType, AmortizationMethod amortizationMethod, PeriodFrequencyType repaymentPeriodFrequencyType, Integer repaymentEvery, - Integer numberOfRepayments, boolean interestRecognitionOnDisbursementDate) { + Integer numberOfRepayments, boolean interestRecognitionOnDisbursementDate, + DaysInYearCustomStrategyType daysInYearCustomStrategy) { this.currency = currency; this.interestRatePerPeriod = interestRatePerPeriod; this.annualNominalInterestRate = annualNominalInterestRate; @@ -69,6 +72,7 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe this.repaymentEvery = repaymentEvery; this.numberOfRepayments = numberOfRepayments; this.interestRecognitionOnDisbursementDate = interestRecognitionOnDisbursementDate; + this.daysInYearCustomStrategy = daysInYearCustomStrategy; } private Integer defaultToNullIfZero(final Integer value) { @@ -168,4 +172,9 @@ public class LoanProductRelatedDetailMinimumData implements LoanProductMinimumRe public boolean isInterestRecognitionOnDisbursementDate() { return interestRecognitionOnDisbursementDate; } + + @Override + public DaysInYearCustomStrategyType getDaysInYearCustomStrategy() { + return daysInYearCustomStrategy; + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java index 56c7a72b1..c5792f700 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProduct.java @@ -56,6 +56,7 @@ import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket; @@ -366,6 +367,9 @@ public class LoanProduct extends AbstractPersistableCustom { final DaysInYearType daysInYearType = DaysInYearType .fromInt(command.integerValueOfParameterNamed(LoanProductConstants.DAYS_IN_YEAR_TYPE_PARAMETER_NAME)); + final DaysInYearCustomStrategyType daysInYearCustomStrategy = command.enumValueOfParameterNamed( + LoanProductConstants.DAYS_IN_YEAR_CUSTOM_STRATEGY_TYPE_PARAMETER_NAME, DaysInYearCustomStrategyType.class); + LoanProductInterestRecalculationDetails interestRecalculationSettings = null; if (isInterestRecalculationEnabled) { @@ -486,7 +490,8 @@ public class LoanProduct extends AbstractPersistableCustom { allowApprovedDisbursedAmountsOverApplied, overAppliedCalculationType, overAppliedNumber, dueDaysForRepaymentEvent, overDueDaysForRepaymentEvent, enableDownPayment, disbursedAmountPercentageDownPayment, enableAutoRepaymentForDownPayment, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType, loanScheduleProcessingType, fixedLength, - enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, interestRecognitionOnDisbursementDate); + enableAccrualActivityPosting, supportedInterestRefundTypes, chargeOffBehaviour, interestRecognitionOnDisbursementDate, + daysInYearCustomStrategy); } @@ -705,7 +710,8 @@ public class LoanProduct extends AbstractPersistableCustom { final boolean enableInstallmentLevelDelinquency, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes, - final LoanChargeOffBehaviour chargeOffBehaviour, final boolean isInterestRecognitionOnDisbursementDate) { + final LoanChargeOffBehaviour chargeOffBehaviour, final boolean isInterestRecognitionOnDisbursementDate, + final DaysInYearCustomStrategyType daysInYearCustomStrategy) { this.fund = fund; this.transactionProcessingStrategyCode = transactionProcessingStrategyCode; @@ -755,7 +761,7 @@ public class LoanProduct extends AbstractPersistableCustom { inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType.getValue(), daysInYearType.getValue(), isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, - supportedInterestRefundTypes, chargeOffBehaviour, isInterestRecognitionOnDisbursementDate); + supportedInterestRefundTypes, chargeOffBehaviour, isInterestRecognitionOnDisbursementDate, daysInYearCustomStrategy); this.loanProductMinMaxConstraints = new LoanProductMinMaxConstraints(defaultMinPrincipal, defaultMaxPrincipal, defaultMinNominalInterestRatePerPeriod, defaultMaxNominalInterestRatePerPeriod, defaultMinNumberOfInstallments, @@ -883,6 +889,18 @@ public class LoanProduct extends AbstractPersistableCustom { } + if (this.getLoanProductRelatedDetail().getDaysInYearCustomStrategy() != null + && this.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE)) { + throw new LoanProductGeneralRuleException("days.in.year.custom.strategy.is.only.supported.for.progressive.loan.schedule.type", + "daysInYearCustomStrategy is only supported for progressive loan schedule type"); + } + + if (this.getLoanProductRelatedDetail().getDaysInYearCustomStrategy() != null + && !this.getLoanProductRelatedDetail().getDaysInYearType().equals(DaysInYearType.ACTUAL.getValue())) { + throw new LoanProductGeneralRuleException("days.in.year.custom.strategy.is.only.applicable.for.actual.days.in.year.type", + "daysInYearCustomStrategy is only applicable for ACTUAL days in year type"); + } + } public MonetaryCurrency getCurrency() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java index 8fddc4a92..08623b60b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductMinimumRepaymentScheduleRelatedDetail.java @@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanproduct.domain; import java.math.BigDecimal; import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; /** @@ -62,4 +63,6 @@ public interface LoanProductMinimumRepaymentScheduleRelatedDetail { Integer getDaysInYearType(); boolean isInterestRecognitionOnDisbursementDate(); + + DaysInYearCustomStrategyType getDaysInYearCustomStrategy(); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java index 82719d2f4..912d75712 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductRelatedDetail.java @@ -35,6 +35,7 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -168,6 +169,10 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche @Column(name = "interest_recognition_on_disbursement_date", nullable = false) private boolean interestRecognitionOnDisbursementDate = false; + @Enumerated(EnumType.STRING) + @Column(name = "days_in_year_custom_strategy") + private DaysInYearCustomStrategyType daysInYearCustomStrategy; + public static LoanProductRelatedDetail createFrom(final CurrencyData currencyData, final BigDecimal principal, final BigDecimal nominalInterestRatePerPeriod, final PeriodFrequencyType interestRatePeriodFrequencyType, final BigDecimal nominalAnnualInterestRate, final InterestMethod interestMethod, @@ -181,7 +186,8 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final boolean enableAutoRepaymentForDownPayment, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, final boolean enableAccrualActivityPosting, final List supportedInterestRefundTypes, - final LoanChargeOffBehaviour chargeOffBehaviour, final boolean interestRecognitionOnDisbursementDate) { + final LoanChargeOffBehaviour chargeOffBehaviour, final boolean interestRecognitionOnDisbursementDate, + final DaysInYearCustomStrategyType daysInYearCustomStrategy) { final MonetaryCurrency currency = MonetaryCurrency.fromCurrencyData(currencyData); return new LoanProductRelatedDetail(currency, principal, nominalInterestRatePerPeriod, interestRatePeriodFrequencyType, @@ -191,7 +197,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche inArrearsTolerance, graceOnArrearsAgeing, daysInMonthType, daysInYearType, isInterestRecalculationEnabled, isEqualAmortization, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, loanScheduleType, loanScheduleProcessingType, fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, - chargeOffBehaviour, interestRecognitionOnDisbursementDate); + chargeOffBehaviour, interestRecognitionOnDisbursementDate, daysInYearCustomStrategy); } protected LoanProductRelatedDetail() { @@ -211,7 +217,8 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche final boolean enableAutoRepaymentForDownPayment, final LoanScheduleType loanScheduleType, final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength, final boolean enableAccrualActivityPosting, List supportedInterestRefundTypes, - final LoanChargeOffBehaviour chargeOffBehaviour, final boolean interestRecognitionOnDisbursementDate) { + final LoanChargeOffBehaviour chargeOffBehaviour, final boolean interestRecognitionOnDisbursementDate, + final DaysInYearCustomStrategyType daysInYearCustomStrategy) { this.currency = currency; this.principal = defaultPrincipal; this.nominalInterestRatePerPeriod = defaultNominalInterestRatePerPeriod; @@ -248,6 +255,7 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche this.supportedInterestRefundTypes = supportedInterestRefundTypes; this.chargeOffBehaviour = chargeOffBehaviour; this.interestRecognitionOnDisbursementDate = interestRecognitionOnDisbursementDate; + this.daysInYearCustomStrategy = daysInYearCustomStrategy; } private Integer defaultToNullIfZero(final Integer value) { @@ -295,6 +303,11 @@ public class LoanProductRelatedDetail implements LoanProductMinimumRepaymentSche : BigDecimal.valueOf(Double.parseDouble(this.annualNominalInterestRate.stripTrailingZeros().toString())); } + @Override + public DaysInYearCustomStrategyType getDaysInYearCustomStrategy() { + return daysInYearCustomStrategy; + } + // TODO: Move into assembler public Map update(final JsonCommand command, final AprCalculator aprCalculator) { diff --git a/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductTest.java b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductTest.java new file mode 100644 index 000000000..6c3f3ffde --- /dev/null +++ b/fineract-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/domain/LoanProductTest.java @@ -0,0 +1,67 @@ +/** + * 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.portfolio.loanproduct.domain; + +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; +import org.apache.fineract.portfolio.loanproduct.exception.LoanProductGeneralRuleException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class LoanProductTest { + + @Test + void testCanCreateLoanProductWithDaysInYearCustomStrategy() { + LoanProduct loanProduct = new LoanProduct(); + loanProduct.setLoanProductRelatedDetail(new LoanProductRelatedDetail()); + loanProduct.getLoanProductRelatedDetail().setLoanScheduleType(LoanScheduleType.PROGRESSIVE); + loanProduct.getLoanProductRelatedDetail().setDaysInYearType(1); + loanProduct.getLoanProductRelatedDetail().setDaysInYearCustomStrategy(DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY); + loanProduct.validateLoanProductPreSave(); + } + + @Test + void testValidationExceptionForCreateCumulativeLoanProductWithDaysInYearCustomStrategy() { + LoanProduct loanProduct = new LoanProduct(); + loanProduct.setLoanProductRelatedDetail(new LoanProductRelatedDetail()); + loanProduct.getLoanProductRelatedDetail().setLoanScheduleType(LoanScheduleType.CUMULATIVE); + loanProduct.getLoanProductRelatedDetail().setDaysInYearType(1); + loanProduct.getLoanProductRelatedDetail().setDaysInYearCustomStrategy(DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY); + Assertions.assertThrowsExactly(LoanProductGeneralRuleException.class, loanProduct::validateLoanProductPreSave); + } + + @Test + void testValidationExceptionForCreate360LoanProductWithDaysInYearCustomStrategy() { + LoanProduct loanProduct = new LoanProduct(); + loanProduct.setLoanProductRelatedDetail(new LoanProductRelatedDetail()); + loanProduct.getLoanProductRelatedDetail().setLoanScheduleType(LoanScheduleType.PROGRESSIVE); + loanProduct.getLoanProductRelatedDetail().setDaysInYearType(360); + loanProduct.getLoanProductRelatedDetail().setDaysInYearCustomStrategy(DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY); + Assertions.assertThrowsExactly(LoanProductGeneralRuleException.class, loanProduct::validateLoanProductPreSave); + } + + @Test + void testCanCreate360LoanProductWithoutDaysInYearCustomStrategy() { + LoanProduct loanProduct = new LoanProduct(); + loanProduct.setLoanProductRelatedDetail(new LoanProductRelatedDetail()); + loanProduct.getLoanProductRelatedDetail().setLoanScheduleType(LoanScheduleType.PROGRESSIVE); + loanProduct.getLoanProductRelatedDetail().setDaysInYearType(360); + loanProduct.validateLoanProductPreSave(); + } +} diff --git a/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java b/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java index 7b3641326..5e08658f6 100644 --- a/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java +++ b/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java @@ -19,6 +19,7 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlan; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanDisbursementPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanDownPaymentPeriod; @@ -56,8 +57,9 @@ public class Main { final Integer installmentAmountInMultiplesOf = null; final Integer fixedLength = null; final Boolean interestRecognitionOnDisbursementDate = false; + final DaysInYearCustomStrategyType dasInYearCustomStrategy = null; - var config = new LoanRepaymentScheduleModelData(startDate, currency, disbursedAmount, disbursementDate, noRepayments, repaymentFrequency, repaymentFrequencyType, annualNominalInterestRate, isDownPaymentEnabled, daysInMonthType, daysInYearType, downPaymentPercentage, installmentAmountInMultiplesOf, fixedLength, interestRecognitionOnDisbursementDate); + var config = new LoanRepaymentScheduleModelData(startDate, currency, disbursedAmount, disbursementDate, noRepayments, repaymentFrequency, repaymentFrequencyType, annualNominalInterestRate, isDownPaymentEnabled, daysInMonthType, daysInYearType, downPaymentPercentage, installmentAmountInMultiplesOf, fixedLength, interestRecognitionOnDisbursementDate, dasInYearCustomStrategy); final LoanSchedulePlan plan = calculator.generate(mc, config); printPlan(plan); diff --git a/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java b/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java index f6b74ffcd..32e7fccb4 100644 --- a/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java +++ b/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java @@ -24,6 +24,7 @@ import java.math.RoundingMode; import java.time.LocalDate; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlan; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanDisbursementPeriod; @@ -55,10 +56,12 @@ class EmbeddableProgressiveLoanScheduleGeneratorTest { final Integer installmentAmountInMultiplesOf = null; final Integer fixedLength = null; final Boolean interestRecognitionOnDisbursementDate = false; + final DaysInYearCustomStrategyType daysInYearCustomStrategy = null; var config = new LoanRepaymentScheduleModelData(startDate, currency, disbursedAmount, disbursementDate, noRepayments, repaymentFrequency, repaymentFrequencyType, annualNominalInterestRate, isDownPaymentEnabled, daysInMonthType, - daysInYearType, downPaymentPercentage, installmentAmountInMultiplesOf, fixedLength, interestRecognitionOnDisbursementDate); + daysInYearType, downPaymentPercentage, installmentAmountInMultiplesOf, fixedLength, interestRecognitionOnDisbursementDate, + daysInYearCustomStrategy); final LoanSchedulePlan plan = calculator.generate(mc, config); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index b7743b549..08c934394 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -37,6 +37,7 @@ import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; @@ -506,6 +507,28 @@ public final class ProgressiveEMICalculator implements EMICalculator { repaymentPeriods.forEach(repaymentPeriod -> calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel)); } + private boolean isPeriodContainsFeb29(final LocalDate repaymentPeriodFromDate, final LocalDate repaymentPeriodDueDate) { + if (repaymentPeriodFromDate.isLeapYear()) { + final LocalDate leapDay = LocalDate.of(repaymentPeriodFromDate.getYear(), 2, 29); + return DateUtils.isDateInRangeFromExclusiveToInclusive(leapDay, repaymentPeriodFromDate, repaymentPeriodDueDate); + } else { + return false; + } + } + + private Integer numberOfDaysFeb29PeriodOnly(final LocalDate repaymentPeriodFromDate, final LocalDate repaymentPeriodDueDate) { + return isPeriodContainsFeb29(repaymentPeriodFromDate, repaymentPeriodDueDate) ? 366 : 365; + } + + private BigDecimal getNumberOfDays(DaysInYearType daysInYearType, DaysInYearCustomStrategyType customStrategy, + LocalDate interestPeriodFromDate, LocalDate repaymentPeriodFromDate, LocalDate repaymentPeriodDueDate) { + Integer numberOfDays = daysInYearType.getNumberOfDays(interestPeriodFromDate); + if (numberOfDays == 366 && DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY.equals(customStrategy)) { + numberOfDays = numberOfDaysFeb29PeriodOnly(repaymentPeriodFromDate, repaymentPeriodDueDate); + } + return BigDecimal.valueOf(numberOfDays); + } + BigDecimal calculateRateFactorPerPeriodForInterest(final ProgressiveLoanInterestScheduleModel scheduleModel, final RepaymentPeriod repaymentPeriod, final LocalDate interestPeriodFromDate, final LocalDate interestPeriodDueDate) { final MathContext mc = scheduleModel.mc(); @@ -515,14 +538,17 @@ public final class ProgressiveEMICalculator implements EMICalculator { final DaysInYearType daysInYearType = DaysInYearType.fromInt(loanProductRelatedDetail.getDaysInYearType()); final DaysInMonthType daysInMonthType = DaysInMonthType.fromInt(loanProductRelatedDetail.getDaysInMonthType()); final PeriodFrequencyType repaymentFrequency = loanProductRelatedDetail.getRepaymentPeriodFrequencyType(); - - BigDecimal daysInYear = BigDecimal.valueOf(daysInYearType.getNumberOfDays(interestPeriodFromDate)); + DaysInYearCustomStrategyType daysInYearCustomStrategy = loanProductRelatedDetail.getDaysInYearCustomStrategy(); + BigDecimal daysInYear = getNumberOfDays(daysInYearType, daysInYearCustomStrategy, interestPeriodFromDate, + repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()); final BigDecimal actualDaysInPeriod = BigDecimal .valueOf(DateUtils.getDifferenceInDays(interestPeriodFromDate, interestPeriodDueDate)); final BigDecimal calculatedDaysInPeriod = BigDecimal .valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate())); final int numberOfYearsDifferenceInPeriod = interestPeriodDueDate.getYear() - interestPeriodFromDate.getYear(); - final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0; + final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0 + && (!DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY.equals(daysInYearCustomStrategy) + || isPeriodContainsFeb29(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate())); // TODO check: loanApplicationTerms.calculatePeriodsBetweenDates(startDate, endDate); // calculate period data // TODO review: (repayment frequency: days, weeks, years; validation day is month fix 30) @@ -628,13 +654,17 @@ public final class ProgressiveEMICalculator implements EMICalculator { final PeriodFrequencyType repaymentFrequency = loanProductRelatedDetail.getRepaymentPeriodFrequencyType(); final BigDecimal repaymentEvery = BigDecimal.valueOf(loanProductRelatedDetail.getRepayEvery()); - BigDecimal daysInYear = BigDecimal.valueOf(daysInYearType.getNumberOfDays(interestPeriodFromDate)); + DaysInYearCustomStrategyType daysInYearCustomStrategy = loanProductRelatedDetail.getDaysInYearCustomStrategy(); + BigDecimal daysInYear = getNumberOfDays(daysInYearType, daysInYearCustomStrategy, interestPeriodFromDate, + repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate()); final BigDecimal actualDaysInPeriod = BigDecimal .valueOf(DateUtils.getDifferenceInDays(interestPeriodFromDate, interestPeriodDueDate)); final BigDecimal calculatedDaysInPeriod = BigDecimal .valueOf(DateUtils.getDifferenceInDays(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate())); final int numberOfYearsDifferenceInPeriod = interestPeriodDueDate.getYear() - interestPeriodFromDate.getYear(); - final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0; + final boolean partialPeriodCalculationNeeded = daysInYearType == DaysInYearType.ACTUAL && numberOfYearsDifferenceInPeriod > 0 + && (!DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY.equals(daysInYearCustomStrategy) + || isPeriodContainsFeb29(repaymentPeriod.getFromDate(), repaymentPeriod.getDueDate())); final BigDecimal daysInMonth = daysInMonthType.isDaysInMonth_30() ? BigDecimal.valueOf(30) : calculatedDaysInPeriod; // TODO check: loanApplicationTerms.calculatePeriodsBetweenDates(startDate, endDate); // calculate period data diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java index 2e3c1a8a4..0548e6dd0 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java @@ -54,7 +54,7 @@ class LoanScheduleGeneratorTest { void testGenerateLoanSchedule() { LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), CURRENCY, DISBURSEMENT_AMOUNT, DISBURSEMENT_DATE, NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE, - NOMINAL_INTEREST_RATE, false, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, null, null, null, false); + NOMINAL_INTEREST_RATE, false, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, null, null, null, false, null); ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator); @@ -89,7 +89,8 @@ class LoanScheduleGeneratorTest { void testGenerateLoanScheduleWithDownPayment() { LoanRepaymentScheduleModelData modelData = new LoanRepaymentScheduleModelData(LocalDate.of(2024, 1, 1), CURRENCY, DISBURSEMENT_AMOUNT_100, LocalDate.of(2024, 1, 1), NUMBER_OF_REPAYMENTS, REPAYMENT_FREQUENCY, REPAYMENT_FREQUENCY_TYPE, - NOMINAL_INTEREST_RATE, true, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, DOWN_PAYMENT_PORTION, null, null, false); + NOMINAL_INTEREST_RATE, true, DaysInMonthType.DAYS_30, DaysInYearType.DAYS_360, DOWN_PAYMENT_PORTION, null, null, false, + null); ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator(); ProgressiveLoanScheduleGenerator generator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator); diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java index a0971cd2f..a20a0054a 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java @@ -30,6 +30,7 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; @@ -45,6 +46,7 @@ import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -91,6 +93,12 @@ class ProgressiveEMICalculatorTest { moneyHelper.close(); } + @BeforeEach + public void setupTestDefaults() { + Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); + Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy()).thenReturn(null); + } + private BigDecimal getRateFactorsByMonth(final DaysInYearType daysInYearType, final DaysInMonthType daysInMonthType, final BigDecimal interestRate, LoanRepaymentScheduleInstallment period) { final BigDecimal daysInPeriod = BigDecimal.valueOf(DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate())); @@ -208,7 +216,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -253,7 +260,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -290,7 +296,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -327,7 +332,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -374,7 +378,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); @@ -417,7 +420,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); @@ -465,7 +467,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); @@ -512,7 +513,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); @@ -559,7 +559,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); @@ -667,7 +666,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); @@ -739,7 +737,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); @@ -819,7 +816,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -869,7 +865,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -919,7 +914,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -966,7 +960,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -1003,7 +996,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -1046,7 +1038,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -1080,7 +1071,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -1114,7 +1104,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.WEEKS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -1148,7 +1137,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.DAYS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(15); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -1191,7 +1179,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -1240,7 +1227,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -1370,7 +1356,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -1444,7 +1429,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -1483,7 +1467,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -1522,7 +1505,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( @@ -1562,7 +1544,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( @@ -1602,7 +1583,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( @@ -1642,7 +1622,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( @@ -1682,7 +1661,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( @@ -1725,7 +1703,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( @@ -1771,7 +1748,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( @@ -1813,7 +1789,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); List loanTermVariations = new ArrayList<>(); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( @@ -1852,7 +1827,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); List loanTermVariationsData = List.of(// new LoanTermVariationsData(1L, LoanTermVariationType.DUE_DATE.getValue(), // LocalDate.of(2024, 2, 1), null, // @@ -1892,7 +1866,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); List loanTermVariationsData = List.of(// new LoanTermVariationsData(1L, LoanTermVariationType.DUE_DATE.getValue(), // LocalDate.of(2024, 2, 1), null, // @@ -1935,7 +1908,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); List loanTermVariationsData = List.of(// new LoanTermVariationsData(1L, LoanTermVariationType.DUE_DATE.getValue(), // LocalDate.of(2024, 3, 1), null, // @@ -1976,7 +1948,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -2026,7 +1997,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -2071,7 +2041,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -2121,7 +2090,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -2159,7 +2127,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -2197,7 +2164,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -2235,7 +2201,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -2273,7 +2238,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -2311,7 +2275,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); @@ -2348,7 +2311,6 @@ class ProgressiveEMICalculatorTest { Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); - Mockito.when(loanProductRelatedDetail.isInterestRecognitionOnDisbursementDate()).thenReturn(false); final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator .generatePeriodInterestScheduleModel(expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), null, mc); @@ -2855,6 +2817,240 @@ class ProgressiveEMICalculatorTest { checkPeriod(interestSchedule, 5, 0, 17.19, 0.005833333333, 0.0996916666611, 0.10, 17.09, 0.0); } + @Nested + class LeapYear366OnlyForPeriodWith29thOfFebruaryTest { + + /** + * February is split and leap year + */ + @Test + public void test_leap_year_only_actual_for_loan_S1() { + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2023, 12, 12), LocalDate.of(2024, 1, 12)), + repayment(1, LocalDate.of(2024, 1, 12), LocalDate.of(2024, 2, 12)), + repayment(1, LocalDate.of(2024, 2, 12), LocalDate.of(2024, 3, 12)), + repayment(1, LocalDate.of(2024, 3, 12), LocalDate.of(2024, 4, 12)), + repayment(1, LocalDate.of(2024, 4, 12), LocalDate.of(2024, 5, 12)), + repayment(1, LocalDate.of(2024, 5, 12), LocalDate.of(2024, 6, 12))); + + final BigDecimal interestRate = BigDecimal.valueOf(9.482); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy()) + .thenReturn(DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(10000.0); + emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2023, 12, 12), disbursedAmount); + + checkPeriod(interestSchedule, 0, 1713.21, 80.53, 1632.68, 8367.32, false); + checkPeriod(interestSchedule, 1, 1713.21, 67.38, 1645.83, 6721.49, false); + checkPeriod(interestSchedule, 2, 1713.21, 50.50, 1662.71, 5058.78, false); + checkPeriod(interestSchedule, 3, 1713.21, 40.74, 1672.47, 3386.31, false); + checkPeriod(interestSchedule, 4, 1713.21, 26.39, 1686.82, 1699.49, false); + checkPeriod(interestSchedule, 5, 1713.18, 13.69, 1699.49, 0.00, false); + + } + + /** + * No february but leap year + */ + @Test + public void test_leap_year_only_actual_for_loan_S2() { + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2024, 7, 23), LocalDate.of(2024, 8, 23)), + repayment(1, LocalDate.of(2024, 8, 23), LocalDate.of(2024, 9, 23)), + repayment(1, LocalDate.of(2024, 9, 23), LocalDate.of(2024, 10, 23)), + repayment(1, LocalDate.of(2024, 10, 23), LocalDate.of(2024, 11, 23))); + + final BigDecimal interestRate = BigDecimal.valueOf(12.0); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy()) + .thenReturn(DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(15000.0); + emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 7, 23), disbursedAmount); + + checkPeriod(interestSchedule, 0, 3845.41, 152.88, 3692.53, 11307.47, false); + checkPeriod(interestSchedule, 1, 3845.41, 115.24, 3730.17, 7577.30, false); + checkPeriod(interestSchedule, 2, 3845.41, 74.74, 3770.67, 3806.63, false); + checkPeriod(interestSchedule, 3, 3845.43, 38.80, 3806.63, 0.00, false); + } + + /** + * February in one period + */ + @Test + public void test_leap_year_only_actual_for_loan_S3() { + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2023, 10, 31), LocalDate.of(2023, 11, 30)), + repayment(1, LocalDate.of(2023, 11, 30), LocalDate.of(2023, 12, 31)), + repayment(1, LocalDate.of(2023, 12, 31), LocalDate.of(2024, 1, 31)), + repayment(1, LocalDate.of(2024, 1, 31), LocalDate.of(2024, 2, 29)), + repayment(1, LocalDate.of(2024, 2, 29), LocalDate.of(2024, 3, 31)), + repayment(1, LocalDate.of(2024, 3, 31), LocalDate.of(2024, 4, 30))); + + final BigDecimal interestRate = BigDecimal.valueOf(45.00); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy()) + .thenReturn(DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(245000.0); + emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2023, 10, 31), disbursedAmount); + + checkPeriod(interestSchedule, 0, 46348.39, 9061.64, 37286.75, 207713.25, false); + checkPeriod(interestSchedule, 1, 46348.39, 7938.63, 38409.76, 169303.49, false); + checkPeriod(interestSchedule, 2, 46348.39, 6470.64, 39877.75, 129425.74, false); + checkPeriod(interestSchedule, 3, 46348.39, 4614.77, 41733.62, 87692.12, false); + checkPeriod(interestSchedule, 4, 46348.39, 3351.52, 42996.87, 44695.25, false); + checkPeriod(interestSchedule, 5, 46348.36, 1653.11, 44695.25, 0.00, false); + + } + + /** + * No Feb month - leap and non leap year split + */ + @Test + public void test_leap_year_only_actual_for_loan_S4() { + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2024, 10, 31), LocalDate.of(2024, 11, 30)), + repayment(1, LocalDate.of(2024, 11, 30), LocalDate.of(2024, 12, 31)), + repayment(1, LocalDate.of(2024, 12, 31), LocalDate.of(2025, 1, 31)), + repayment(1, LocalDate.of(2025, 1, 31), LocalDate.of(2025, 2, 28)), + repayment(1, LocalDate.of(2025, 2, 28), LocalDate.of(2025, 3, 31)), + repayment(1, LocalDate.of(2025, 3, 31), LocalDate.of(2025, 4, 30))); + + final BigDecimal interestRate = BigDecimal.valueOf(9.99); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy()) + .thenReturn(DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(2450); + emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 10, 31), disbursedAmount); + + checkPeriod(interestSchedule, 0, 420.24, 20.12, 400.12, 2049.88, false); + checkPeriod(interestSchedule, 1, 420.24, 17.39, 402.85, 1647.03, false); + checkPeriod(interestSchedule, 2, 420.24, 13.97, 406.27, 1240.76, false); + checkPeriod(interestSchedule, 3, 420.24, 9.51, 410.73, 830.03, false); + checkPeriod(interestSchedule, 4, 420.24, 7.04, 413.20, 416.83, false); + checkPeriod(interestSchedule, 5, 420.25, 3.42, 416.83, 0.00, false); + } + + /** + * no leap year + */ + @Test + public void test_leap_year_only_actual_for_loan_S5() { + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2022, 10, 29), LocalDate.of(2022, 12, 29)), + repayment(1, LocalDate.of(2022, 12, 29), LocalDate.of(2023, 2, 28)), + repayment(1, LocalDate.of(2023, 2, 28), LocalDate.of(2023, 4, 29)), + repayment(1, LocalDate.of(2023, 4, 29), LocalDate.of(2023, 6, 29)), + repayment(1, LocalDate.of(2023, 6, 29), LocalDate.of(2023, 8, 29)), + repayment(1, LocalDate.of(2023, 8, 29), LocalDate.of(2023, 10, 29))); + + final BigDecimal interestRate = BigDecimal.valueOf(7.00); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy()) + .thenReturn(DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.ACTUAL.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(2); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(5000.0); + emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2022, 10, 29), disbursedAmount); + + checkPeriod(interestSchedule, 0, 867.68, 58.49, 809.19, 4190.81, false); + checkPeriod(interestSchedule, 1, 867.68, 49.03, 818.65, 3372.16, false); + checkPeriod(interestSchedule, 2, 867.68, 38.80, 828.88, 2543.28, false); + checkPeriod(interestSchedule, 3, 867.68, 29.75, 837.93, 1705.35, false); + checkPeriod(interestSchedule, 4, 867.68, 19.95, 847.73, 857.62, false); + checkPeriod(interestSchedule, 5, 867.65, 10.03, 857.62, 0.00, false); + } + + @Test + public void test_leap_year_only_actual_no_effect_on_360_loan() { + final List expectedRepaymentPeriods = List.of( + repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)), + repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1)), + repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)), + repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1)), + repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1)), + repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); + + final BigDecimal interestRate = BigDecimal.valueOf(7.0); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrencyData()).thenReturn(currency); + Mockito.when(loanProductRelatedDetail.getDaysInYearCustomStrategy()) + .thenReturn(DaysInYearCustomStrategyType.FEB_29_PERIOD_ONLY); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, List.of(), installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(100.0); + emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); + + checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.5833333333, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 1, 0, 17.01, 0.005833333333, 0.487491666639, 0.49, 16.52, 67.05); + checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.391124999979, 0.39, 16.62, 50.43); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.294174999983, 0.29, 16.72, 33.71); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.196641666655, 0.2, 16.81, 16.90); + checkPeriod(interestSchedule, 5, 0, 17.00, 0.005833333333, 0.0985833333276, 0.1, 16.9, 0.0); + } + } + @Test public void test_s5_chargeback_in_period_Amt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = List.of( diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/loan/LoanImportHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/loan/LoanImportHandler.java index dea87fc35..119dc2a5b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/loan/LoanImportHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/loan/LoanImportHandler.java @@ -351,7 +351,7 @@ public class LoanImportHandler implements ImportHandler { nominalInterestRate, submittedOnDate, amortizationEnumOption, interestMethodEnum, interestCalculationPeriodEnum, arrearsTolerance, repaymentStrategyCode, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, firstRepaymentOnDate, row.getRowNum(), externalId, null, charges, linkAccountId, locale, - dateFormat, loanCollateralManagementData, null); + dateFormat, loanCollateralManagementData, null, null); } else if (loanType.equals("jlg")) { Long clientId = ImportHandlerUtils.getIdByName(workbook.getSheet(TemplatePopulateImportConstants.CLIENT_SHEET_NAME), clientOrGroupName); @@ -360,7 +360,7 @@ public class LoanImportHandler implements ImportHandler { nominalInterestRate, submittedOnDate, amortizationEnumOption, interestMethodEnum, interestCalculationPeriodEnum, arrearsTolerance, repaymentStrategyCode, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, interestChargedFromDate, firstRepaymentOnDate, row.getRowNum(), externalId, groupId, charges, linkAccountId, locale, - dateFormat, null, null); + dateFormat, null, null, null); } else { Long groupIdforGroupLoan = ImportHandlerUtils .getIdByName(workbook.getSheet(TemplatePopulateImportConstants.GROUP_SHEET_NAME), clientOrGroupName); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index 767a452de..ef4a8c075 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -109,6 +109,7 @@ import org.apache.fineract.portfolio.client.data.ClientData; import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; import org.apache.fineract.portfolio.collateralmanagement.data.LoanCollateralResponseData; import org.apache.fineract.portfolio.collateralmanagement.service.LoanCollateralManagementReadPlatformService; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiResourceSwagger; import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData; @@ -244,7 +245,8 @@ public class LoansApiResource { LoanApiConstants.isTopup, LoanApiConstants.loanIdToClose, LoanApiConstants.topupAmount, LoanApiConstants.clientActiveLoanOptions, LoanApiConstants.datatables, LoanProductConstants.RATES_PARAM_NAME, LoanApiConstants.MULTIDISBURSE_DETAILS_PARAMNAME, LoanApiConstants.EMI_AMOUNT_VARIATIONS_PARAMNAME, - LoanApiConstants.COLLECTION_PARAMNAME, LoanApiConstants.INTEREST_RECOGNITION_ON_DISBURSEMENT_DATE)); + LoanApiConstants.COLLECTION_PARAMNAME, LoanApiConstants.INTEREST_RECOGNITION_ON_DISBURSEMENT_DATE, + LoanApiConstants.daysInYearCustomStrategyParameterName)); private static final Set LOAN_APPROVAL_DATA_PARAMETERS = new HashSet<>(Arrays.asList("approvalDate", "approvalAmount")); private static final Set GLIM_ACCOUNTS_DATA_PARAMETERS = new HashSet<>(Arrays.asList("glimId", "groupId", "clientId", @@ -1133,7 +1135,7 @@ public class LoansApiResource { loanCollateralOptions, calendarOptions, notes, accountLinkingOptions, linkedAccount, disbursementData, emiAmountVariations, overdueCharges, paidInAdvanceTemplate, interestRatesPeriods, clientActiveLoanOptions, rates, isRatesEnabled, collectionData, LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList(), - loanTermVariations); + loanTermVariations, DaysInYearCustomStrategyType.getValuesAsStringEnumOptionDataList()); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters(), mandatoryResponseParameters); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index de873637f..8fdf8c460 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -1273,6 +1273,8 @@ final class LoansApiResourceSwagger { public String transactionProcessingStrategyCode; @Schema(example = "360", allowableValues = "1, 360, 364, 36") public Integer daysInYearType; + @Schema(example = "FULL_LEAP_YEAR", allowableValues = "FULL_LEAP_YEAR, FEB_29_PERIOD_ONLY") + public String daysInYearCustomStrategy; @Schema(example = "individual") public String loanType; @Schema(example = "20 September 2011") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java index 6928bd298..1d7a6f21c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanAccountData.java @@ -171,6 +171,7 @@ public class LoanAccountData { private Collection calendarOptions; private List loanScheduleTypeOptions; private List loanScheduleProcessingTypeOptions; + private List daysInYearCustomStrategyOptions; @Transient private BigDecimal feeChargesAtDisbursementCharged; @@ -216,6 +217,7 @@ public class LoanAccountData { private EnumOptionData daysInMonthType; private EnumOptionData daysInYearType; + private StringEnumOptionData daysInYearCustomStrategy; // TODO: avoid prefix "is" private boolean isInterestRecalculationEnabled; @@ -282,7 +284,7 @@ public class LoanAccountData { Integer graceOnInterestPayment, Integer graceOnInterestCharged, LocalDate interestChargedFromDate, LocalDate repaymentsStartingFromDate, Integer rowIndex, ExternalId externalId, Long groupId, Collection charges, String linkAccountId, String locale, String dateFormat, List loanCollateralManagementData, - Integer fixedLength) { + Integer fixedLength, StringEnumOptionData daysInYearCustomStrategy) { return new LoanAccountData().setLoanType(loanTypeEnumOption).setClientId(clientId).setProductId(productId) .setLoanOfficerId(loanOfficerId).setSubmittedOnDate(submittedOnDate).setFundId(fundId).setPrincipal(principal) @@ -296,7 +298,8 @@ public class LoanAccountData { .setGraceOnInterestCharged(graceOnInterestCharged).setInterestChargedFromDate(interestChargedFromDate) .setRepaymentsStartingFromDate(repaymentsStartingFromDate).setRowIndex(rowIndex).setExternalId(externalId) .setGroupId(groupId).setCharges(charges).setLinkAccountId(linkAccountId).setLocale(locale).setDateFormat(dateFormat) - .setCollateral(loanCollateralManagementData).setFixedLength(fixedLength); + .setCollateral(loanCollateralManagementData).setFixedLength(fixedLength) + .setDaysInYearCustomStrategy(daysInYearCustomStrategy); } public static LoanAccountData importInstanceGroup(EnumOptionData loanTypeEnumOption, Long groupIdforGroupLoan, Long productId, @@ -421,7 +424,9 @@ public class LoanAccountData { .setFixedPrincipalPercentagePerInstallment(product.getFixedPrincipalPercentagePerInstallment()) .setDelinquent(CollectionData.template()).setDisallowExpectedDisbursements(product.getDisallowExpectedDisbursements()) .setLoanScheduleType(product.getLoanScheduleType()).setLoanScheduleProcessingType(product.getLoanScheduleProcessingType()) - .setInterestRecognitionOnDisbursementDate(product.isInterestRecognitionOnDisbursementDate()); + .setInterestRecognitionOnDisbursementDate(product.isInterestRecognitionOnDisbursementDate()) + .setDaysInYearCustomStrategyOptions(product.getDaysInYearCustomStrategyOptions()) + .setDaysInYearCustomStrategy(product.getDaysInYearCustomStrategy()); } /* @@ -459,7 +464,7 @@ public class LoanAccountData { final BigDecimal disbursedAmountPercentageForDownPayment, final boolean enableAutoRepaymentForDownPayment, final boolean enableInstallmentLevelDelinquency, final EnumOptionData loanScheduleType, final EnumOptionData loanScheduleProcessingType, final Integer fixedLength, final StringEnumOptionData chargeOffBehaviour, - final boolean isInterestRecognitionOnDisbursementDate) { + final boolean isInterestRecognitionOnDisbursementDate, final StringEnumOptionData daysInYearCustomStrategy) { final CollectionData delinquent = CollectionData.template(); @@ -504,8 +509,8 @@ public class LoanAccountData { .setEnableAutoRepaymentForDownPayment(enableAutoRepaymentForDownPayment) .setEnableInstallmentLevelDelinquency(enableInstallmentLevelDelinquency).setLoanScheduleType(loanScheduleType) .setLoanScheduleProcessingType(loanScheduleProcessingType).setFixedLength(fixedLength) - .setChargeOffBehaviour(chargeOffBehaviour) - .setInterestRecognitionOnDisbursementDate(isInterestRecognitionOnDisbursementDate); + .setChargeOffBehaviour(chargeOffBehaviour).setInterestRecognitionOnDisbursementDate(isInterestRecognitionOnDisbursementDate) + .setDaysInYearCustomStrategy(daysInYearCustomStrategy); } /* @@ -530,7 +535,8 @@ public class LoanAccountData { final PaidInAdvanceData paidInAdvance, Collection interestRatesPeriods, final Collection clientActiveLoanOptions, final List rates, final Boolean isRatesEnabled, final CollectionData delinquent, final List loanScheduleTypeOptions, - final List loanScheduleProcessingTypeOptions, final List loanTermVariations) { + final List loanScheduleProcessingTypeOptions, final List loanTermVariations, + final List daysInYearCustomStrategyOptions) { // TODO: why are these variables 'calendarData', 'chargeTemplate' never used (see original private constructor) @@ -550,7 +556,8 @@ public class LoanAccountData { .setOverdueCharges(overdueCharges).setPaidInAdvance(paidInAdvance).setInterestRatesPeriods(interestRatesPeriods) .setClientActiveLoanOptions(clientActiveLoanOptions).setRates(rates).setIsRatesEnabled(isRatesEnabled) .setDelinquent(delinquent).setLoanScheduleTypeOptions(loanScheduleTypeOptions) - .setLoanScheduleProcessingTypeOptions(loanScheduleProcessingTypeOptions).setLoanTermVariations(loanTermVariations); + .setLoanScheduleProcessingTypeOptions(loanScheduleProcessingTypeOptions).setLoanTermVariations(loanTermVariations) + .setDaysInYearCustomStrategyOptions(daysInYearCustomStrategyOptions); } public LoanAccountData associationsAndTemplate(final Collection productOptions, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index 47d2b0eb4..802989844 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -541,7 +541,8 @@ public class LoanScheduleAssembler { loanScheduleType, loanScheduleProcessingType, fixedLength, loanProduct.getLoanProductRelatedDetail().isEnableAccrualActivityPosting(), loanProduct.getLoanProductRelatedDetail().getSupportedInterestRefundTypes(), - loanProduct.getLoanProductRelatedDetail().getChargeOffBehaviour(), interestRecognitionOnDisbursementDate); + loanProduct.getLoanProductRelatedDetail().getChargeOffBehaviour(), interestRecognitionOnDisbursementDate, + loanProduct.getLoanProductRelatedDetail().getDaysInYearCustomStrategy()); } private CalendarInstance createCalendarForSameAsRepayment(final Integer repaymentEvery, 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 a1b497afd..c3e5741e9 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 @@ -83,6 +83,8 @@ import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollatera import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagementRepositoryWrapper; import org.apache.fineract.portfolio.collateralmanagement.domain.CollateralManagementDomain; import org.apache.fineract.portfolio.collateralmanagement.service.LoanCollateralAssembler; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; +import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper; import org.apache.fineract.portfolio.group.exception.ClientNotInGroupException; @@ -170,7 +172,7 @@ public final class LoanApplicationValidator { LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, LoanProductConstants.FIXED_LENGTH, LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, LoanProductConstants.ENABLE_DOWN_PAYMENT, LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT, - LoanApiConstants.INTEREST_RECOGNITION_ON_DISBURSEMENT_DATE)); + LoanApiConstants.INTEREST_RECOGNITION_ON_DISBURSEMENT_DATE, LoanApiConstants.daysInYearCustomStrategyParameterName)); public static final String LOANAPPLICATION_UNDO = "loanapplication.undo"; private final FromJsonHelper fromApiJsonHelper; @@ -752,6 +754,26 @@ public final class LoanApplicationValidator { loanProductDataValidator.validateRepaymentPeriodWithGraceSettings(numberOfRepayments, graceOnPrincipalPayment, graceOnInterestPayment, graceOnInterestCharged, recurringMoratoriumOnPrincipalPeriods, baseDataValidator); } + + if (fromApiJsonHelper.parameterExists(LoanApiConstants.daysInYearCustomStrategyParameterName, element)) { + DaysInYearCustomStrategyType daysInYearCustomStrategy = fromApiJsonHelper + .enumValueOfParameterNamed("daysInYearCustomStrategyParameterName", element, DaysInYearCustomStrategyType.class); + if (daysInYearCustomStrategy != null) { + if (!LoanScheduleType.PROGRESSIVE.equals(loanProduct.getLoanProductRelatedDetail().getLoanScheduleType())) { + baseDataValidator.reset().parameter(LoanApiConstants.daysInYearCustomStrategyParameterName).failWithCode( + "days.in.year.custom.strategy.is.only.supported.for.progressive.loan.schedule.type", + "daysInYearCustomStrategy is only supported for progressive loan schedule type"); + } + + if (!DaysInYearType.ACTUAL.getValue().equals(loanProduct.getLoanProductRelatedDetail().getDaysInYearType())) { + baseDataValidator.reset().parameter(LoanApiConstants.daysInYearCustomStrategyParameterName).failWithCode( + "days.in.year.custom.strategy.is.only.applicable.for.actual.days.in.year.type", + "daysInYearCustomStrategy is only applicable for ACTUAL days in year type"); + } + + } + } + }); validateSubmittedOnDate(element, null, null, loanProduct); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index cbf86250d..a985a74bc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -42,6 +42,7 @@ import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.data.EnumOptionData; +import org.apache.fineract.infrastructure.core.data.StringEnumOptionData; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -74,6 +75,7 @@ import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; import org.apache.fineract.portfolio.client.data.ClientData; import org.apache.fineract.portfolio.client.domain.ClientEnumerations; import org.apache.fineract.portfolio.client.service.ClientReadPlatformService; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.service.CommonEnumerations; import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; @@ -695,6 +697,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa + " lir.disallow_interest_calc_on_past_due as disallowInterestCalculationOnPastDue, " + " l.is_floating_interest_rate as isFloatingInterestRate, " + " l.interest_rate_differential as interestRateDifferential, " + + " l.days_in_year_custom_strategy as daysInYearCustomStrategy, " + " l.create_standing_instruction_at_disbursement as createStandingInstructionAtDisbursement, " + " lpvi.minimum_gap as minimuminstallmentgap, lpvi.maximum_gap as maximuminstallmentgap, " + " lp.can_use_for_topup as canUseForTopup, l.is_topup as isTopup, topup.closure_loan_id as closureLoanId, " @@ -1069,6 +1072,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final Integer fixedLength = JdbcSupport.getInteger(rs, "fixedLength"); final LoanChargeOffBehaviour chargeOffBehaviour = LoanChargeOffBehaviour.valueOf(rs.getString("chargeOffBehaviour")); final boolean interestRecognitionOnDisbursementDate = rs.getBoolean("interestRecognitionOnDisbursementDate"); + final StringEnumOptionData daysInYearCustomStrategy = DaysInYearCustomStrategyType + .getStringEnumOptionData(rs.getString("daysInYearCustomStrategy")); return LoanAccountData.basicLoanDetails(id, accountNo, status, externalId, clientId, clientAccountNo, clientName, clientOfficeId, clientExternalId, groupData, loanType, loanProductId, loanProductName, loanProductDescription, @@ -1088,7 +1093,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa lastClosedBusinessDate, overpaidOnDate, isChargedOff, enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment, enableInstallmentLevelDelinquency, loanScheduleType.asEnumOptionData(), loanScheduleProcessingType.asEnumOptionData(), fixedLength, chargeOffBehaviour.getValueAsStringEnumOptionData(), - interestRecognitionOnDisbursementDate); + interestRecognitionOnDisbursementDate, daysInYearCustomStrategy); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java index b68a40005..f45322979 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/api/LoanProductsApiResource.java @@ -72,6 +72,7 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.service.CurrencyReadPlatformService; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.service.DropdownReadPlatformService; import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; @@ -126,7 +127,7 @@ public class LoanProductsApiResource { LoanApiConstants.fixedPrincipalPercentagePerInstallmentParamName, LoanProductConstants.DUE_DAYS_FOR_REPAYMENT_EVENT, LoanProductConstants.OVER_DUE_DAYS_FOR_REPAYMENT_EVENT, LoanProductConstants.ENABLE_DOWN_PAYMENT, LoanProductConstants.DISBURSED_AMOUNT_PERCENTAGE_DOWN_PAYMENT, LoanProductConstants.ENABLE_AUTO_REPAYMENT_DOWN_PAYMENT, - LoanProductConstants.REPAYMENT_START_DATE_TYPE)); + LoanProductConstants.REPAYMENT_START_DATE_TYPE, LoanProductConstants.DAYS_IN_YEAR_CUSTOM_STRATEGY_TYPE_PARAMETER_NAME)); private static final Set PRODUCT_MIX_DATA_PARAMETERS = new HashSet<>( Arrays.asList("productId", "productName", "restrictedProducts", "allowedProducts", "productOptions")); @@ -436,6 +437,8 @@ public class LoanProductsApiResource { final List chargeOffBehaviourOptions = LoanChargeOffBehaviour.getValuesAsStringEnumOptionDataList(); final List chargeOffReasonOptions = codeValueReadPlatformService .retrieveCodeValuesByCode(LoanApiConstants.CHARGE_OFF_REASONS); + final List daysInYearCustomStrategyOptions = DaysInYearCustomStrategyType + .getValuesAsStringEnumOptionDataList(); return new LoanProductData(productData, chargeOptions, penaltyOptions, paymentTypeOptions, currencyOptions, amortizationTypeOptions, interestTypeOptions, interestCalculationPeriodTypeOptions, repaymentFrequencyTypeOptions, interestRateFrequencyTypeOptions, @@ -447,7 +450,8 @@ public class LoanProductsApiResource { advancedPaymentAllocationTransactionTypes, advancedPaymentAllocationFutureInstallmentAllocationRules, advancedPaymentAllocationTypes, LoanScheduleType.getValuesAsEnumOptionDataList(), LoanScheduleProcessingType.getValuesAsEnumOptionDataList(), creditAllocationTransactionTypes, - creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffBehaviourOptions, chargeOffReasonOptions); + creditAllocationAllocationTypes, supportedInterestRefundTypesOptions, chargeOffBehaviourOptions, chargeOffReasonOptions, + daysInYearCustomStrategyOptions); } } 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 aeab409e6..e8733e6be 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 @@ -185,7 +185,8 @@ public final class LoanProductDataValidator { LoanProductConstants.ENABLE_INSTALLMENT_LEVEL_DELINQUENCY, LoanProductConstants.LOAN_SCHEDULE_TYPE, LoanProductConstants.LOAN_SCHEDULE_PROCESSING_TYPE, LoanProductConstants.FIXED_LENGTH, LoanProductConstants.ENABLE_ACCRUAL_ACTIVITY_POSTING, LoanProductConstants.SUPPORTED_INTEREST_REFUND_TYPES, - LoanProductConstants.CHARGE_OFF_BEHAVIOUR, LoanProductConstants.INTEREST_RECOGNITION_ON_DISBURSEMENT_DATE)); + LoanProductConstants.CHARGE_OFF_BEHAVIOUR, LoanProductConstants.INTEREST_RECOGNITION_ON_DISBURSEMENT_DATE, + LoanProductConstants.DAYS_IN_YEAR_CUSTOM_STRATEGY_TYPE_PARAMETER_NAME)); private static final String[] SUPPORTED_LOAN_CONFIGURABLE_ATTRIBUTES = { LoanProductConstants.amortizationTypeParamName, LoanProductConstants.interestTypeParamName, LoanProductConstants.transactionProcessingStrategyCodeParamName, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java index 466b33a0c..c3fe4c728 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanProductReadPlatformServiceImpl.java @@ -41,6 +41,7 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.charge.data.ChargeData; import org.apache.fineract.portfolio.charge.service.ChargeReadPlatformService; +import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType; import org.apache.fineract.portfolio.common.service.CommonEnumerations; import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; @@ -270,6 +271,7 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo + "lp.fixed_principal_percentage_per_installment fixedPrincipalPercentagePerInstallment, " + "lp.sync_expected_with_disbursement_date as syncExpectedWithDisbursementDate, " + "lpg.id as lpgId, lpg.mandatory_guarantee as mandatoryGuarantee, " + + "lp.days_in_year_custom_strategy as daysInYearCustomStrategy, " + "lpg.minimum_guarantee_from_own_funds as minimumGuaranteeFromOwnFunds, lpg.minimum_guarantee_from_guarantor_funds as minimumGuaranteeFromGuarantor, " + "lp.account_moves_out_of_npa_only_on_arrears_completion as accountMovesOutOfNPAOnlyOnArrearsCompletion, " + "curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, curr.display_symbol as currencyDisplaySymbol, lp.external_id as externalId, " @@ -428,6 +430,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo final int daysInMonth = JdbcSupport.getInteger(rs, "daysInMonth"); final EnumOptionData daysInMonthType = CommonEnumerations.daysInMonthType(daysInMonth); final int daysInYear = JdbcSupport.getInteger(rs, "daysInYear"); + final StringEnumOptionData daysInYearCustomStrategy = DaysInYearCustomStrategyType + .getStringEnumOptionData(rs.getString("daysInYearCustomStrategy")); final EnumOptionData daysInYearType = CommonEnumerations.daysInYearType(daysInYear); final Integer installmentAmountInMultiplesOf = JdbcSupport.getInteger(rs, "installmentAmountInMultiplesOf"); final boolean canDefineInstallmentAmount = rs.getBoolean("canDefineInstallmentAmount"); @@ -571,7 +575,8 @@ public class LoanProductReadPlatformServiceImpl implements LoanProductReadPlatfo enableAutoRepaymentForDownPayment, advancedPaymentData, creditAllocationData, repaymentStartDateType, enableInstallmentLevelDelinquency, loanScheduleType.asEnumOptionData(), loanScheduleProcessingType.asEnumOptionData(), fixedLength, enableAccrualActivityPosting, supportedInterestRefundTypes, - loanChargeOffBehaviour.getValueAsStringEnumOptionData(), interestRecognitionOnDisbursementDate); + loanChargeOffBehaviour.getValueAsStringEnumOptionData(), interestRecognitionOnDisbursementDate, + daysInYearCustomStrategy); } } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index f757c5772..2f294c33c 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -188,4 +188,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0170_days_in_year_custom_strategy.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0170_days_in_year_custom_strategy.xml new file mode 100644 index 000000000..c4c01a8d5 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0170_days_in_year_custom_strategy.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java index b1f93d432..e4798530f 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/DefaultScheduledDateGeneratorTest.java @@ -98,7 +98,7 @@ public class DefaultScheduledDateGeneratorTest { Money.of(fromApplicationCurrency(dollarCurrency), ZERO), false, null, EMPTY_LIST, BigDecimal.valueOf(36_000L), null, DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null, null, ZERO, null, NONE, null, ZERO, EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false, null, false, DISBURSEMENT_DATE, - submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null, null, false); + submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null, null, false, null); // when List result = underTest.generateRepaymentPeriods(mathContext, expectedDisbursementDate, @@ -170,7 +170,7 @@ public class DefaultScheduledDateGeneratorTest { EMPTY_LIST, BigDecimal.valueOf(36_000L), null, DaysInMonthType.ACTUAL, DaysInYearType.ACTUAL, false, null, null, null, null, null, ZERO, null, NONE, null, ZERO, EMPTY_LIST, true, 0, false, holidayDetailDTO, false, false, false, null, false, false, null, false, DISBURSEMENT_DATE, submittedOnDate, CUMULATIVE, LoanScheduleProcessingType.HORIZONTAL, null, false, null, null, - false); + false, null); } private HolidayDetailDTO createHolidayDTO() { 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 372607070..ee4e6681d 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 @@ -1500,4 +1500,10 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest { return advancedPaymentData; } + protected static class DaysInYearCustomStrategy { + + public static String FEB_29_PERIOD_ONLY = "FEB_29_PERIOD_ONLY"; + public static String FULL_LEAP_YEAR = "FULL_LEAP_YEAR"; + } + } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DaysInYearCustomStrategyTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DaysInYearCustomStrategyTest.java new file mode 100644 index 000000000..e330d0a4d --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DaysInYearCustomStrategyTest.java @@ -0,0 +1,100 @@ +/** + * 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 java.io.IOException; +import java.math.BigDecimal; +import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.FineractClientHelper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import retrofit2.Call; +import retrofit2.Response; + +public class DaysInYearCustomStrategyTest extends BaseLoanIntegrationTest { + + private final Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getResourceId(); + + @Test + public void test_HttpError_for_ValidationError_DaysInYearsCustomStrategy() { + Call loanProductCall = FineractClientHelper.getFineractClient().loanProducts + .createLoanProduct(create4IProgressive() + // invalid settings combination + .daysInYearType(DaysInYearType.DAYS_360).daysInYearCustomStrategy(DaysInYearCustomStrategy.FEB_29_PERIOD_ONLY)); + try { + Response response = loanProductCall.execute(); + Assertions.assertEquals(403, response.code()); + } catch (IOException e) { + Assertions.fail("Unexpected exception", e); + } + } + + @Test + public void testFEB_29_PERIOD_ONLY() { + runAt("1 January 2024", () -> { + PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(create4IProgressive().currencyCode("USD") + .daysInYearType(DaysInYearType.ACTUAL).daysInYearCustomStrategy(DaysInYearCustomStrategy.FEB_29_PERIOD_ONLY)); + Long loanId = applyAndApproveProgressiveLoan(clientId, loanProduct.getResourceId(), "1 January 2024", 10000.0, 99.99, 12, null); + Assertions.assertNotNull(loanId); + disburseLoan(loanId, BigDecimal.valueOf(10000), "1 January 2024"); + verifyRepaymentSchedule(loanId, // + installment(10000.0, null, "01 January 2024"), // + installment(519.92, 821.84, 1341.76, false, "01 February 2024"), // + installment(564.78, 776.98, 1341.76, false, "01 March 2024"), // + installment(609.07, 732.69, 1341.76, false, "01 April 2024"), // + installment(659.12, 682.64, 1341.76, false, "01 May 2024"), // + installment(713.29, 628.47, 1341.76, false, "01 June 2024"), // + installment(771.91, 569.85, 1341.76, false, "01 July 2024"), // + installment(835.35, 506.41, 1341.76, false, "01 August 2024"), // + installment(904.0, 437.76, 1341.76, false, "01 September 2024"), // + installment(978.3, 363.46, 1341.76, false, "01 October 2024"), // + installment(1058.7, 283.06, 1341.76, false, "01 November 2024"), // + installment(1145.71, 196.05, 1341.76, false, "01 December 2024"), // + installment(1239.85, 101.9, 1341.75, false, "01 January 2025") // + ); + }); + } + + @Test + public void testFULL_LEAP_YEAR() { + runAt("1 January 2024", () -> { + PostLoanProductsResponse loanProduct = loanProductHelper.createLoanProduct(create4IProgressive().currencyCode("USD") + .daysInYearType(DaysInYearType.ACTUAL).daysInYearCustomStrategy(DaysInYearCustomStrategy.FULL_LEAP_YEAR)); + Long loanId = applyAndApproveProgressiveLoan(clientId, loanProduct.getResourceId(), "1 January 2024", 10000.0, 99.99, 12, null); + Assertions.assertNotNull(loanId); + disburseLoan(loanId, BigDecimal.valueOf(10000), "1 January 2024"); + verifyRepaymentSchedule(loanId, // + installment(10000.0, null, "01 January 2024"), // + installment(521.04, 819.59, 1340.63, false, "01 February 2024"), // + installment(563.74, 776.89, 1340.63, false, "01 March 2024"), // + installment(609.95, 730.68, 1340.63, false, "01 April 2024"), // + installment(659.94, 680.69, 1340.63, false, "01 May 2024"), // + installment(714.03, 626.6, 1340.63, false, "01 June 2024"), // + installment(772.55, 568.08, 1340.63, false, "01 July 2024"), // + installment(835.86, 504.77, 1340.63, false, "01 August 2024"), // + installment(904.37, 436.26, 1340.63, false, "01 September 2024"), // + installment(978.49, 362.14, 1340.63, false, "01 October 2024"), // + installment(1058.69, 281.94, 1340.63, false, "01 November 2024"), // + installment(1145.46, 195.17, 1340.63, false, "01 December 2024"), // + installment(1235.88, 104.68, 1340.56, false, "01 January 2025") // + ); + }); + } +}