FINERACT-2197: Introduce a new type of ACTUAL during calculation of days in year
This commit is contained in:
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+1
-10
@@ -362,16 +362,7 @@ public final class JsonCommand {
|
||||
}
|
||||
|
||||
public <T extends Enum<T>> T enumValueOfParameterNamed(String parameterName, Class<T> 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) {
|
||||
|
||||
+16
@@ -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 extends Enum<T>> T enumValueOfParameterNamed(String parameterName, final JsonElement element, Class<T> enumType) {
|
||||
String value = null;
|
||||
try {
|
||||
value = this.helperDelegator.extractStringNamed(parameterName, element, new HashSet<String>());
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+78
@@ -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.
|
||||
* <p>
|
||||
* This enum has two modes:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>{@link #FULL_LEAP_YEAR} - Always considers 366 days in a leap year.</li>
|
||||
* <li>{@link #FEB_29_PERIOD_ONLY} - Considers 366 days only if the period includes February 29th; otherwise, uses 365
|
||||
* days.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* When using {@link DaysInYearType#ACTUAL}, the {@code FEB_29_PERIOD_ONLY} rule applies:
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li>If the period includes February 29th, the calculation considers 366 days.</li>
|
||||
* <li>Otherwise, even if the year is a leap year, the calculation uses 365 days.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* <b>Examples:</b>
|
||||
* </p>
|
||||
* <ul>
|
||||
* <li><b>366 Days:</b> A period from February 10th to March 10th in a leap year will consider 366 days, since February
|
||||
* 29th is within the period.</li>
|
||||
* <li><b>365 Days:</b> A period from March 10th to April 10th in a leap year will consider 365 days, as February 29th
|
||||
* is not within the period.</li>
|
||||
* </ul>
|
||||
*/
|
||||
@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<StringEnumOptionData> 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();
|
||||
}
|
||||
}
|
||||
+1
@@ -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, //
|
||||
|
||||
+29
@@ -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<PostLoanProductsResponse> 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,
|
||||
|
||||
+1
@@ -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";
|
||||
|
||||
@@ -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 |
|
||||
+3
-3
@@ -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
|
||||
|
||||
+1
@@ -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";
|
||||
|
||||
+19
-7
@@ -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<LoanSupportedInterestRefundTypes> 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<LoanSupportedInterestRefundTypes> 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<LoanSupportedInterestRefundTypes> 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() {
|
||||
|
||||
+3
-11
@@ -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) {
|
||||
}
|
||||
|
||||
+1
@@ -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";
|
||||
|
||||
+29
@@ -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<Integer> 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<GetLoanProductsValueConditionTypeOptions> valueConditionTypeOptions;
|
||||
public Set<GetLoansProductsDaysInMonthTemplateType> daysInMonthTypeOptions;
|
||||
public Set<GetLoanProductsInterestTemplateType> daysInYearTypeOptions;
|
||||
public Set<GetLoanProductsDaysInYearCustomStrategyTemplateType> daysInYearTypeCustomStrategyOptions;
|
||||
public Set<GetLoanProductsResponse.GetLoanProductsInterestRecalculationData.GetLoanProductsInterestRecalculationCompoundingType> interestRecalculationCompoundingTypeOptions;
|
||||
public Set<GetLoanProductsResponse.GetLoanProductsInterestRecalculationData.GetLoanProductsRescheduleStrategyType> rescheduleStrategyTypeOptions;
|
||||
public Set<GetLoanProductsResponse.GetLoanProductsInterestRecalculationData.GetLoanProductsInterestRecalculationCompoundingFrequencyType> interestRecalculationFrequencyTypeOptions;
|
||||
|
||||
+19
-8
@@ -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<StringEnumOptionData> 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<StringEnumOptionData> 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<StringEnumOptionData> 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<StringEnumOptionData> 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<StringEnumOptionData> 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<String, Object> 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<StringEnumOptionData> 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<ChargeData> chargeOptions,
|
||||
@@ -965,7 +973,8 @@ public class LoanProductData implements Serializable {
|
||||
final List<EnumOptionData> loanScheduleProcessingTypeOptions, final List<EnumOptionData> creditAllocationTransactionTypes,
|
||||
final List<EnumOptionData> creditAllocationAllocationTypes,
|
||||
final List<StringEnumOptionData> supportedInterestRefundTypesOptions,
|
||||
final List<StringEnumOptionData> chargeOffBehaviourOptions, final List<CodeValueData> chargeOffReasonOptions) {
|
||||
final List<StringEnumOptionData> chargeOffBehaviourOptions, final List<CodeValueData> chargeOffReasonOptions,
|
||||
final List<StringEnumOptionData> 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<ChargeData> nullIfEmpty(final Collection<ChargeData> charges) {
|
||||
|
||||
+10
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+21
-3
@@ -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<Long> {
|
||||
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<Long> {
|
||||
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<Long> {
|
||||
final boolean enableInstallmentLevelDelinquency, final LoanScheduleType loanScheduleType,
|
||||
final LoanScheduleProcessingType loanScheduleProcessingType, final Integer fixedLength,
|
||||
final boolean enableAccrualActivityPosting, final List<LoanSupportedInterestRefundTypes> 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<Long> {
|
||||
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<Long> {
|
||||
|
||||
}
|
||||
|
||||
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() {
|
||||
|
||||
+3
@@ -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();
|
||||
}
|
||||
|
||||
+16
-3
@@ -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<LoanSupportedInterestRefundTypes> 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<LoanSupportedInterestRefundTypes> 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<String, Object> update(final JsonCommand command, final AprCalculator aprCalculator) {
|
||||
|
||||
|
||||
+67
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+4
-1
@@ -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);
|
||||
|
||||
|
||||
+35
-5
@@ -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
|
||||
|
||||
+3
-2
@@ -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);
|
||||
|
||||
+242
-46
@@ -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<LoanTermVariationsData> 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<LoanTermVariationsData> 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<LoanTermVariationsData> 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<LoanTermVariationsData> 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<LoanTermVariationsData> 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<LoanTermVariationsData> 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<LoanTermVariationsData> 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<LoanTermVariationsData> 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> 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> 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> 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<LoanScheduleModelRepaymentPeriod> 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<LoanScheduleModelRepaymentPeriod> 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<LoanScheduleModelRepaymentPeriod> 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<LoanScheduleModelRepaymentPeriod> 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<LoanScheduleModelRepaymentPeriod> 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<LoanScheduleModelRepaymentPeriod> 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<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = List.of(
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+4
-2
@@ -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<String> LOAN_APPROVAL_DATA_PARAMETERS = new HashSet<>(Arrays.asList("approvalDate", "approvalAmount"));
|
||||
private static final Set<String> 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);
|
||||
|
||||
+2
@@ -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")
|
||||
|
||||
+15
-8
@@ -171,6 +171,7 @@ public class LoanAccountData {
|
||||
private Collection<CalendarData> calendarOptions;
|
||||
private List<EnumOptionData> loanScheduleTypeOptions;
|
||||
private List<EnumOptionData> loanScheduleProcessingTypeOptions;
|
||||
private List<StringEnumOptionData> 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<LoanChargeData> charges,
|
||||
String linkAccountId, String locale, String dateFormat, List<LoanCollateralManagementData> 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<InterestRatePeriodData> interestRatesPeriods,
|
||||
final Collection<LoanAccountSummaryData> clientActiveLoanOptions, final List<RateData> rates, final Boolean isRatesEnabled,
|
||||
final CollectionData delinquent, final List<EnumOptionData> loanScheduleTypeOptions,
|
||||
final List<EnumOptionData> loanScheduleProcessingTypeOptions, final List<LoanTermVariationsData> loanTermVariations) {
|
||||
final List<EnumOptionData> loanScheduleProcessingTypeOptions, final List<LoanTermVariationsData> loanTermVariations,
|
||||
final List<StringEnumOptionData> 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<LoanProductData> productOptions,
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+23
-1
@@ -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);
|
||||
|
||||
+6
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -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<String> PRODUCT_MIX_DATA_PARAMETERS = new HashSet<>(
|
||||
Arrays.asList("productId", "productName", "restrictedProducts", "allowedProducts", "productOptions"));
|
||||
@@ -436,6 +437,8 @@ public class LoanProductsApiResource {
|
||||
final List<StringEnumOptionData> chargeOffBehaviourOptions = LoanChargeOffBehaviour.getValuesAsStringEnumOptionDataList();
|
||||
final List<CodeValueData> chargeOffReasonOptions = codeValueReadPlatformService
|
||||
.retrieveCodeValuesByCode(LoanApiConstants.CHARGE_OFF_REASONS);
|
||||
final List<StringEnumOptionData> 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+6
-1
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -188,4 +188,5 @@
|
||||
<include file="parts/0167_create_m_calendar_instance_index.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0168_transaction_summary_with_asset_owner_report_add_active_intermediate_filtering.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0169_add_missing_permissions.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0170_days_in_year_custom_strategy.xml" relativeToChangelogFile="true"/>
|
||||
</databaseChangeLog>
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<addColumn tableName="m_loan">
|
||||
<column name="days_in_year_custom_strategy" type="VARCHAR(100)">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<addColumn tableName="m_product_loan">
|
||||
<column name="days_in_year_custom_strategy" type="VARCHAR(100)">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+2
-2
@@ -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<? extends LoanScheduleModelPeriod> 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() {
|
||||
|
||||
+6
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+100
@@ -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<PostLoanProductsResponse> loanProductCall = FineractClientHelper.getFineractClient().loanProducts
|
||||
.createLoanProduct(create4IProgressive()
|
||||
// invalid settings combination
|
||||
.daysInYearType(DaysInYearType.DAYS_360).daysInYearCustomStrategy(DaysInYearCustomStrategy.FEB_29_PERIOD_ONLY));
|
||||
try {
|
||||
Response<PostLoanProductsResponse> 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") //
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user