diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java index 166560395..5419a7902 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/savings/DepositsApiConstants.java @@ -112,6 +112,11 @@ public final class DepositsApiConstants { public static final String routingCodeParamName = "routingCode"; public static final String receiptNumberParamName = "receiptNumber"; public static final String bankNumberParamName = "bankNumber"; + public static final String principalAmountParamName = "principalAmount"; + public static final String annualInterestRateParamName = "annualInterestRate"; + public static final String interestPostingPeriodInMonthsParamName = "interestPostingPeriodInMonths"; + public static final String tenureInMonthsParamName = "tenureInMonths"; + public static final String interestCompoundingPeriodInMonthsParamName = "interestCompoundingPeriodInMonths"; // Preclosure parameters public static final String preClosurePenalApplicableParamName = "preClosurePenalApplicable"; @@ -307,10 +312,22 @@ public final class DepositsApiConstants { public static final Set FIXED_DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = fixedDepositAccountRequestData(); public static final Set FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS = fixedDepositAccountResponseData(); - + public static final Set FIXED_DEPOSIT_ACCOUNT_INTEREST_CALCULATION_PARAMETERS = fixedDepositInterestCalculationData(); public static final Set RECURRING_DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS = recurringDepositAccountRequestData(); public static final Set RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS = recurringDepositAccountResponseData(); + private static Set fixedDepositInterestCalculationData() { + final Set fixedDepositInterestCalculationData = new HashSet<>(); + fixedDepositInterestCalculationData.add(principalAmountParamName); + fixedDepositInterestCalculationData.add(annualInterestRateParamName); + fixedDepositInterestCalculationData.add(tenureInMonthsParamName); + fixedDepositInterestCalculationData.add(interestPostingPeriodInMonthsParamName); + fixedDepositInterestCalculationData.add(interestCompoundingPeriodInMonthsParamName); + + return fixedDepositInterestCalculationData; + + } + private static Set fixedDepositAccountRequestData() { final Set fixedDepositRequestData = new HashSet<>(); fixedDepositRequestData.addAll(DEPOSIT_ACCOUNT_REQUEST_DATA_PARAMETERS); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java index e202dd54e..3c1479555 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResource.java @@ -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 savingCharges = this.savingsAccountChargeReadPlatformService diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResourceSwagger.java index 3249c2b81..74a130cac 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountsApiResourceSwagger.java @@ -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 { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountDataValidator.java index a28eab439..806036a98 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositAccountDataValidator.java @@ -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>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, + DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_INTEREST_CALCULATION_PARAMETERS); + final List 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 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 diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositAccountInterestCalculationService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositAccountInterestCalculationService.java new file mode 100644 index 000000000..5bfd07ab8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositAccountInterestCalculationService.java @@ -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); +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositAccountInterestCalculationServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositAccountInterestCalculationServiceImpl.java new file mode 100644 index 000000000..56c786224 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/FixedDepositAccountInterestCalculationServiceImpl.java @@ -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); + } +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/service/FixedDepositAccountInterestCalculationServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/service/FixedDepositAccountInterestCalculationServiceImplTest.java new file mode 100644 index 000000000..943b46026 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/savings/service/FixedDepositAccountInterestCalculationServiceImplTest.java @@ -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 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); + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedDepositTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedDepositTest.java index 69741dcb5..51ce8d99d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedDepositTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedDepositTest.java @@ -20,14 +20,19 @@ package org.apache.fineract.integrationtests; import static java.time.temporal.ChronoUnit.DAYS; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; import com.google.common.truth.Truth; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.ResponseSpecBuilder; import io.restassured.http.ContentType; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.math.BigDecimal; +import java.math.MathContext; import java.math.RoundingMode; import java.text.DateFormat; import java.text.SimpleDateFormat; @@ -44,6 +49,9 @@ import java.util.Locale; import java.util.TimeZone; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity; +import org.apache.fineract.infrastructure.core.api.JsonQuery; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.SchedulerJobHelper; @@ -62,10 +70,15 @@ import org.apache.fineract.integrationtests.common.fixeddeposit.FixedDepositProd import org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper; import org.apache.fineract.integrationtests.common.savings.SavingsProductHelper; import org.apache.fineract.integrationtests.common.savings.SavingsStatusChecker; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.savings.data.DepositAccountDataValidator; +import org.apache.fineract.portfolio.savings.service.FixedDepositAccountInterestCalculationServiceImpl; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; @Slf4j @SuppressWarnings({ "unused", "unchecked", "rawtypes", "static-access" }) @@ -80,6 +93,8 @@ public class FixedDepositTest { private JournalEntryHelper journalEntryHelper; private FinancialActivityAccountHelper financialActivityAccountHelper; + private FixedDepositAccountInterestCalculationServiceImpl fixedDepositAccountInterestCalculationServiceImpl; + public static final String WHOLE_TERM = "1"; public static final String TILL_PREMATURE_WITHDRAWAL = "2"; private static final String DAILY = "1"; @@ -114,6 +129,8 @@ public class FixedDepositTest { // and then to compare the exact results public static final Float THRESHOLD = 1.0f; + private MockedStatic moneyHelperStatic; + @BeforeEach public void setup() { Utils.initializeRESTAssured(); @@ -126,6 +143,83 @@ public class FixedDepositTest { TimeZone.setDefault(TimeZone.getTimeZone(Utils.TENANT_TIME_ZONE)); } + /*** + * Test case for Fixed Deposit Account Interest Calculation + */ + @Test + public void testFixedDepositInterestCalculationWithWrongCompoundingPeriod() { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("principalAmount", 100); + jsonObject.addProperty("annualInterestRate", 5); + jsonObject.addProperty("tenureInMonths", 12); + jsonObject.addProperty("interestPostingPeriodInMonths", 3); + jsonObject.addProperty("interestCompoundingPeriodInMonths", 7); + JsonParser parser = new JsonParser(); + String apiRequestBodyAsJson = jsonObject.toString(); + JsonElement element = parser.parse(apiRequestBodyAsJson); + moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class); + moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP)); + fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl( + new DepositAccountDataValidator(new FromJsonHelper(), null), new FromJsonHelper()); + try { + HashMap h = fixedDepositAccountInterestCalculationServiceImpl + .calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper())); + fail("The function must throw an exception when called with invalid Compounding period"); + } catch (PlatformApiDataValidationException e) { + assertEquals("Validation errors exist.", e.getMessage()); + } finally { + moneyHelperStatic.close(); + } + } + + @Test + public void testFixedDepositInterestCalculationWithWrongCompoundingPeriod2() { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("principalAmount", 100); + jsonObject.addProperty("annualInterestRate", 5); + jsonObject.addProperty("tenureInMonths", 15); + jsonObject.addProperty("interestPostingPeriodInMonths", 3); + jsonObject.addProperty("interestCompoundingPeriodInMonths", 6); + JsonParser parser = new JsonParser(); + String apiRequestBodyAsJson = jsonObject.toString(); + JsonElement element = parser.parse(apiRequestBodyAsJson); + moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class); + moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP)); + fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl( + new DepositAccountDataValidator(new FromJsonHelper(), null), new FromJsonHelper()); + try { + HashMap h = fixedDepositAccountInterestCalculationServiceImpl + .calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper())); + fail("The function must throw an exception when called with invalid Compounding period"); + } catch (PlatformApiDataValidationException e) { + assertEquals("Validation errors exist.", e.getMessage()); + } finally { + moneyHelperStatic.close(); + } + } + + @Test + public void testFixedDepositInterestCalculationWithValidInput() { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("principalAmount", 10000); + jsonObject.addProperty("annualInterestRate", 5); + jsonObject.addProperty("tenureInMonths", 12); + jsonObject.addProperty("interestPostingPeriodInMonths", 3); + jsonObject.addProperty("interestCompoundingPeriodInMonths", 6); + JsonParser parser = new JsonParser(); + String apiRequestBodyAsJson = jsonObject.toString(); + JsonElement element = parser.parse(apiRequestBodyAsJson); + moneyHelperStatic = Mockito.mockStatic(MoneyHelper.class); + moneyHelperStatic.when(() -> MoneyHelper.getMathContext()).thenReturn(new MathContext(12, RoundingMode.UP)); + fixedDepositAccountInterestCalculationServiceImpl = new FixedDepositAccountInterestCalculationServiceImpl( + new DepositAccountDataValidator(new FromJsonHelper(), null), new FromJsonHelper()); + BigDecimal expectedResult = new BigDecimal("10506.250000"); + BigDecimal actualResult = new BigDecimal(fixedDepositAccountInterestCalculationServiceImpl + .calculateInterest(new JsonQuery(apiRequestBodyAsJson, element, new FromJsonHelper())).get("maturityAmount").toString()); + assertEquals(expectedResult, actualResult); + moneyHelperStatic.close(); + } + /*** * Test case for Fixed Deposit Product with default attributes */