Fineract-1964: Add functionality to calculate maturity amount before creating FD account

This commit is contained in:
goyalrocks007
2024-03-26 09:57:17 +01:00
committed by Adam Saghy
parent 3753d89444
commit 86bd2763f0
8 changed files with 416 additions and 1 deletions
@@ -46,6 +46,7 @@ import java.io.InputStream;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import lombok.RequiredArgsConstructor;
@@ -79,6 +80,7 @@ import org.apache.fineract.portfolio.savings.data.SavingsAccountChargeData;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
import org.apache.fineract.portfolio.savings.service.DepositAccountPreMatureCalculationPlatformService;
import org.apache.fineract.portfolio.savings.service.DepositAccountReadPlatformService;
import org.apache.fineract.portfolio.savings.service.FixedDepositAccountInterestCalculationService;
import org.apache.fineract.portfolio.savings.service.SavingsAccountChargeReadPlatformService;
import org.glassfish.jersey.media.multipart.FormDataContentDisposition;
import org.glassfish.jersey.media.multipart.FormDataParam;
@@ -102,6 +104,7 @@ public class FixedDepositAccountsApiResource {
private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService;
private final BulkImportWorkbookService bulkImportWorkbookService;
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
private final FixedDepositAccountInterestCalculationService fixedDepositAccountInterestCalculationService;
@GET
@Path("template")
@@ -208,6 +211,33 @@ public class FixedDepositAccountsApiResource {
DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
}
@GET
@Path("calculate-fd-interest")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = FixedDepositAccountsApiResourceSwagger.CalculateFixedDepositInterestResponse.class))) })
public String calculateFixedDepositInterest(@Context final UriInfo uriInfo,
@QueryParam("principalAmount") @Parameter(description = "BigDecimal principalAmount") final BigDecimal principalAmount,
@QueryParam("annualInterestRate") @Parameter(description = "annualInterestRate") final BigDecimal annualInterestRate,
@QueryParam("tenureInMonths") @Parameter(description = "tenureInMonths") final Long tenureInMonths,
@QueryParam("interestCompoundingPeriodInMonths") @Parameter(description = "interestCompoundingPeriodInMonths") final Long interestCompoundingPeriodInMonths,
@QueryParam("interestPostingPeriodInMonths") @Parameter(description = "interestPostingPeriodInMonths") final Long interestPostingPeriodInMonths) {
HashMap request = new HashMap<>();
request.put("annualInterestRate", annualInterestRate);
request.put("tenureInMonths", tenureInMonths);
request.put("interestCompoundingPeriodInMonths", interestCompoundingPeriodInMonths);
request.put("interestPostingPeriodInMonths", interestPostingPeriodInMonths);
request.put("principalAmount", principalAmount);
String apiRequestBodyAsJson = toApiJsonSerializer.serialize(request);
JsonElement jsonElement = fromJsonHelper.parse(apiRequestBodyAsJson);
HashMap result = fixedDepositAccountInterestCalculationService
.calculateInterest(new JsonQuery(apiRequestBodyAsJson, jsonElement, fromJsonHelper));
return toApiJsonSerializer.serializeResult(result);
}
private BigDecimal getActivationCharge(Long accountId) {
BigDecimal activationCharge = BigDecimal.ZERO;
Collection<SavingsAccountChargeData> savingCharges = this.savingsAccountChargeReadPlatformService
@@ -19,6 +19,7 @@
package org.apache.fineract.portfolio.savings.api;
import io.swagger.v3.oas.annotations.media.Schema;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Set;
@@ -256,6 +257,36 @@ final class FixedDepositAccountsApiResourceSwagger {
public GetFixedDepositAccountsDepositPeriodFrequency depositPeriodFrequency;
}
@Schema(description = "CalculateFixedDepositInterestRequest")
public static final class CalculateFixedDepositInterestRequest {
private CalculateFixedDepositInterestRequest() {}
@Schema(example = "10000")
public BigDecimal principalAmount;
@Schema(example = "5")
public BigDecimal annualInterestRate;
@Schema(example = "12")
public Long tenureInMonths;
@Schema(example = "3")
public Long interestPostingPeriodInMonths;
@Schema(example = "1")
public Long interestCompoundingPeriodInMonths;
}
@Schema(description = "CalculateFixedDepositInterestResponse")
public static final class CalculateFixedDepositInterestResponse {
private CalculateFixedDepositInterestResponse() {}
@Schema(example = "10511.61")
public BigDecimal maturityAmount;
@Schema(example = "Accuracy Warning")
public String warning;
}
@Schema(description = "PostFixedDepositAccountsRequest")
public static final class PostFixedDepositAccountsRequest {
@@ -20,11 +20,14 @@ package org.apache.fineract.portfolio.savings.data;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.adjustAdvanceTowardsFuturePaymentsParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.allowWithdrawalParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.annualInterestRateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositAmountParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositPeriodFrequencyIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositPeriodParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.inMultiplesOfDepositTermParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.inMultiplesOfDepositTermTypeIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.interestCompoundingPeriodInMonthsParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.interestPostingPeriodInMonthsParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isCalendarInheritedParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.isMandatoryDepositParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.linkedAccountParamName;
@@ -37,8 +40,10 @@ import static org.apache.fineract.portfolio.savings.DepositsApiConstants.minDepo
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClosurePenalApplicableParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClosurePenalInterestOnTypeIdParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.preClosurePenalInterestParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.recurringFrequencyParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.recurringFrequencyTypeParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferInterestToSavingsParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.transferToSavingsIdParamName;
import static org.apache.fineract.portfolio.savings.SavingsApiConstants.accountNoParamName;
@@ -201,6 +206,23 @@ public class DepositAccountDataValidator {
}
public void validateFixedDepositForInterestCalculation(final String json) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json,
DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_INTEREST_CALCULATION_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors)
.resource(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME);
final JsonElement element = this.fromApiJsonHelper.parse(json);
validateForInterestCalc(element, baseDataValidator);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void validateDepositDetailsForSubmit(final JsonElement element, final DataValidatorBuilder baseDataValidator) {
final Long clientId = this.fromApiJsonHelper.extractLongNamed(clientIdParamName, element);
@@ -582,6 +604,40 @@ public class DepositAccountDataValidator {
}
}
private void validateForInterestCalc(final JsonElement element, final DataValidatorBuilder baseDataValidator) {
BigDecimal principalAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalAmountParamName, element);
baseDataValidator.reset().parameter(principalAmountParamName).value(principalAmount).notNull().positiveAmount();
BigDecimal annualInterestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(annualInterestRateParamName, element);
baseDataValidator.reset().parameter(annualInterestRateParamName).value(annualInterestRate).notNull()
.notLessThanMin(BigDecimal.valueOf(0));
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
baseDataValidator.reset().parameter(tenureInMonthsParamName).value(tenureInMonths).notNull().longGreaterThanZero();
Long interestPostingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestPostingPeriodInMonthsParamName, element);
baseDataValidator.reset().parameter(interestPostingPeriodInMonthsParamName).value(interestPostingPeriodInMonths).notNull()
.longGreaterThanZero();
Long interestCompoundingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestCompoundingPeriodInMonthsParamName,
element);
baseDataValidator.reset().parameter(interestCompoundingPeriodInMonthsParamName).value(interestCompoundingPeriodInMonths).notNull();
List<Long> allowedValues = List.of(1L, 2L, 3L, 4L, 6L, 12L);
if (interestCompoundingPeriodInMonths != null && !allowedValues.contains(interestCompoundingPeriodInMonths)) {
baseDataValidator.reset().parameter(interestCompoundingPeriodInMonthsParamName).value(interestCompoundingPeriodInMonths)
.failWithCode("parameter.value.error", "interestCompoundingPeriodInMonths can only be one {1,2,3,4,6,12}");
}
if (interestCompoundingPeriodInMonths != null && tenureInMonths != null
&& tenureInMonths % interestCompoundingPeriodInMonths != 0) {
baseDataValidator.reset().parameter(interestCompoundingPeriodInMonthsParamName).value(interestCompoundingPeriodInMonths)
.failWithCode("parameter.relational.validation",
"tenureInMonths must be perfectly divisible by interestCompoundingPeriodInMonths");
}
}
private void validateRecurringDetailForSubmit(final JsonElement element, final DataValidatorBuilder baseDataValidator) {
final BigDecimal mandatoryRecommendedDepositAmount = fromApiJsonHelper
@@ -0,0 +1,28 @@
/**
* 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.savings.service;
import java.util.HashMap;
import org.apache.fineract.infrastructure.core.api.JsonQuery;
public interface FixedDepositAccountInterestCalculationService {
HashMap calculateInterest(JsonQuery query);
}
@@ -0,0 +1,74 @@
/**
* 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.savings.service;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.annualInterestRateParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.interestCompoundingPeriodInMonthsParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.principalAmountParamName;
import static org.apache.fineract.portfolio.savings.DepositsApiConstants.tenureInMonthsParamName;
import com.google.gson.JsonElement;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.HashMap;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonQuery;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.savings.data.DepositAccountDataValidator;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class FixedDepositAccountInterestCalculationServiceImpl implements FixedDepositAccountInterestCalculationService {
private final DepositAccountDataValidator depositAccountDataValidator;
private final FromJsonHelper fromApiJsonHelper;
@Override
public HashMap calculateInterest(JsonQuery query) {
depositAccountDataValidator.validateFixedDepositForInterestCalculation(query.json());
JsonElement element = query.parsedJson();
BigDecimal principalAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(principalAmountParamName, element);
BigDecimal annualInterestRate = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(annualInterestRateParamName, element);
Long tenureInMonths = this.fromApiJsonHelper.extractLongNamed(tenureInMonthsParamName, element);
Long interestCompoundingPeriodInMonths = this.fromApiJsonHelper.extractLongNamed(interestCompoundingPeriodInMonthsParamName,
element);
BigDecimal maturityAmount = this.calculateInterestInternal(principalAmount, annualInterestRate, tenureInMonths,
interestCompoundingPeriodInMonths);
String warning = "This is an approximate calculated amount - it may vary slightly when the account is created";
HashMap result = new HashMap<>();
result.put("maturityAmount", maturityAmount);
result.put("warning", warning);
return result;
}
public BigDecimal calculateInterestInternal(BigDecimal principalAmount, BigDecimal annualInterestRate, Long tenureInMonths,
Long interestCompoundingPeriodInMonths) {
BigDecimal numberOfCompoundingsPerAnnum = BigDecimal.valueOf(12).divide(BigDecimal.valueOf(interestCompoundingPeriodInMonths));
Long totalNumberOfCompoundings = tenureInMonths / interestCompoundingPeriodInMonths;
MathContext mc = MoneyHelper.getMathContext();
BigDecimal exponentialTerm = annualInterestRate.divide(numberOfCompoundingsPerAnnum, mc).divide(BigDecimal.valueOf(100), mc)
.add(BigDecimal.valueOf(1)).pow(Math.toIntExact(totalNumberOfCompoundings));
return principalAmount.multiply(exponentialTerm);
}
}
@@ -0,0 +1,85 @@
/**
* 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.savings.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.savings.data.DepositAccountDataValidator;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class FixedDepositAccountInterestCalculationServiceImplTest {
private FixedDepositAccountInterestCalculationServiceImpl service;
@Mock
private DepositAccountDataValidator depositAccountDataValidator;
@Mock
private FromJsonHelper fromApiJsonHelper;
private MockedStatic<MoneyHelper> moneyHelperStatic;
@BeforeEach
public void setUp() {
moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class);
moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP));
service = new FixedDepositAccountInterestCalculationServiceImpl(depositAccountDataValidator, fromApiJsonHelper);
}
@AfterEach
public void deregister() {
moneyHelperStatic.close();
}
@Test
public void testCalculateInterestInternal1() {
// Calculate interest
BigDecimal expectedInterest = new BigDecimal("10509.4533691406250000"); // Expected interest calculated based
// on provided values
BigDecimal calculatedInterest = service.calculateInterestInternal(BigDecimal.valueOf(10000), BigDecimal.valueOf(5), 12L, 3L);
// Verify the result
assertEquals(expectedInterest, calculatedInterest);
}
@Test
public void testCalculateInterestInternal2() {
// Calculate interest
BigDecimal expectedInterest = new BigDecimal("105.062500"); // Expected interest calculated based on provided
// values
BigDecimal calculatedInterest = service.calculateInterestInternal(BigDecimal.valueOf(100), BigDecimal.valueOf(5), 12L, 6L);
// Verify the result
assertEquals(expectedInterest, calculatedInterest);
}
}