From 35ebe6ec0059d071d6acf3db37846512b887f490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sandro=20Mart=C3=ADnez?= Date: Wed, 10 Jun 2026 01:07:37 -0600 Subject: [PATCH] FINERACT-1183: archive FD and RD products --- .../savings/DepositsApiConstants.java | 13 +- .../api/FixedDepositAccountsApiResource.java | 2 +- .../api/FixedDepositProductsApiResource.java | 2 +- ...ixedDepositProductsApiResourceSwagger.java | 30 ++ .../RecurringDepositProductsApiResource.java | 2 +- ...ringDepositProductsApiResourceSwagger.java | 28 ++ .../data/DepositProductDataValidator.java | 8 +- .../domain/DepositAccountAssembler.java | 19 ++ .../DepositAccountDomainServiceJpa.java | 3 + ...DepositProductReadPlatformServiceImpl.java | 14 +- .../savings/starter/SavingsConfiguration.java | 5 +- .../db/changelog/tenant/changelog-tenant.xml | 1 + ...237_add_deposit_product_validity_dates.xml | 35 +++ .../savings/data/DepositProductData.java | 35 ++- .../savings/data/FixedDepositProductData.java | 21 +- .../data/RecurringDepositProductData.java | 26 +- .../domain/DepositProductAssembler.java | 12 +- .../savings/domain/FixedDepositProduct.java | 43 ++- .../domain/RecurringDepositProduct.java | 18 +- ...epositAccountApplicationDateException.java | 29 ++ .../FeignDepositProductArchivingTest.java | 267 ++++++++++++++++++ 21 files changed, 565 insertions(+), 48 deletions(-) create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0237_add_deposit_product_validity_dates.xml create mode 100644 fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/DepositAccountApplicationDateException.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignDepositProductArchivingTest.java 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 0f44056280e..8d3434ba870 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 @@ -69,6 +69,8 @@ private DepositsApiConstants() { public static final String localeParamName = "locale"; public static final String dateFormatParamName = "dateFormat"; public static final String monthDayFormatParamName = "monthDayFormat"; + public static final String startDateParamName = "startDate"; + public static final String closeDateParamName = "closeDate"; // deposit product and account parameters public static final String idParamName = "id"; @@ -217,10 +219,11 @@ private DepositsApiConstants() { * Deposit Product Parameters */ private static final Set DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, - monthDayFormatParamName, nameParamName, shortNameParamName, descriptionParamName, currencyCodeParamName, - digitsAfterDecimalParamName, inMultiplesOfParamName, nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, - interestPostingPeriodTypeParamName, interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, - lockinPeriodFrequencyParamName, lockinPeriodFrequencyTypeParamName, accountingRuleParamName, chargesParamName, + monthDayFormatParamName, dateFormatParamName, startDateParamName, closeDateParamName, nameParamName, shortNameParamName, + descriptionParamName, currencyCodeParamName, digitsAfterDecimalParamName, inMultiplesOfParamName, + nominalAnnualInterestRateParamName, interestCompoundingPeriodTypeParamName, interestPostingPeriodTypeParamName, + interestCalculationTypeParamName, interestCalculationDaysInYearTypeParamName, lockinPeriodFrequencyParamName, + lockinPeriodFrequencyTypeParamName, accountingRuleParamName, chargesParamName, SavingProductAccountingParams.INCOME_FROM_FEES.getValue(), SavingProductAccountingParams.INCOME_FROM_PENALTIES.getValue(), SavingProductAccountingParams.INTEREST_ON_SAVINGS.getValue(), SavingProductAccountingParams.PAYMENT_CHANNEL_FUND_SOURCE_MAPPING.getValue(), @@ -277,6 +280,7 @@ private static Set fixedDepositProductResponseData() { fixedDepositRequestData.addAll(DEPOSIT_PRODUCT_REQUEST_DATA_PARAMETERS); fixedDepositRequestData.addAll(PRECLOSURE_RESPONSE_DATA_PARAMETERS); fixedDepositRequestData.addAll(DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS); + fixedDepositRequestData.add(statusParamName); return fixedDepositRequestData; } @@ -296,6 +300,7 @@ private static Set recurringDepositProductResponseData() { recurringDepositRequestData.addAll(PRECLOSURE_RESPONSE_DATA_PARAMETERS); recurringDepositRequestData.addAll(DEPOSIT_TERM_RESPONSE_DATA_PARAMETERS); recurringDepositRequestData.addAll(RECURRING_DETAILS_RESPONSE_DATA_PARAMETERS); + recurringDepositRequestData.add(statusParamName); recurringDepositRequestData.add(SavingsApiConstants.minBalanceForInterestCalculationParamName); return recurringDepositRequestData; } 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 beea6b01356..00355293248 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 @@ -182,7 +182,7 @@ public String retrieveAll(@Context final UriInfo uriInfo, @QueryParam("paged") @ @POST @Consumes({ MediaType.APPLICATION_JSON }) @Produces({ MediaType.APPLICATION_JSON }) - @Operation(summary = "Submit new fixed deposit application", description = """ + @Operation(summary = "Submit new fixed deposit application", operationId = "submitApplicationFixedDepositAccount", description = """ Submits a new fixed deposit application Mandatory Fields: clientId or groupId, productId, submittedOnDate, depositAmount, depositPeriod, depositPeriodFrequencyId diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResource.java index 870cfafc068..779c46ea4fc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResource.java @@ -119,7 +119,7 @@ public class FixedDepositProductsApiResource { Mandatory Fields: name, shortName, description, currencyCode, digitsAfterDecimal,inMultiplesOf, interestCompoundingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minDepositTerm, minDepositTermTypeId, accountingRule - Optional Fields: lockinPeriodFrequency, lockinPeriodFrequencyType, maxDepositTerm, maxDepositTermTypeId, inMultiplesOfDepositTerm, inMultiplesOfDepositTermTypeId, preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnTypeId, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, charges, charts, , withHoldTax, taxGroupId + Optional Fields: startDate, closeDate, lockinPeriodFrequency, lockinPeriodFrequencyType, maxDepositTerm, maxDepositTermTypeId, inMultiplesOfDepositTerm, inMultiplesOfDepositTermTypeId, preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnTypeId, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, charges, charts, , withHoldTax, taxGroupId Mandatory Fields for Cash based accounting (accountingRule = 2): savingsReferenceAccountId, savingsControlAccountId, interestOnSavingsAccountId, incomeFromFeeAccountId, transfersInSuspenseAccountId, incomeFromPenaltyAccountId""") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResourceSwagger.java index 7b0f058d0b2..efa14672b70 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositProductsApiResourceSwagger.java @@ -69,6 +69,10 @@ private PostFixedDepositProductsChartSlabs() {} public String shortName; @Schema(example = "Daily compounding using Daily Balance, 5% per year, 365 days in year") public String description; + @Schema(example = "10 July 2022") + public String startDate; + @Schema(example = "10 July 2022") + public String closeDate; @Schema(example = "USD") public String currencyCode; @Schema(example = "2") @@ -77,6 +81,8 @@ private PostFixedDepositProductsChartSlabs() {} public Integer inMultiplesOf; @Schema(example = "en") public String locale; + @Schema(example = "dd MMMM yyyy") + public String dateFormat; @Schema(example = "1") public Integer interestCompoundingPeriodType; @Schema(example = "4") @@ -101,6 +107,8 @@ private PostFixedDepositProductsChartSlabs() {} public Integer maxDepositTerm; @Schema(example = "3") public Integer maxDepositTermTypeId; + @Schema(example = "10000") + public Long depositAmount; public Set charts; } @@ -120,8 +128,14 @@ private PutFixedDepositProductsProductIdRequest() {} @Schema(example = "Fixed deposit product new offerings") public String description; + @Schema(example = "10 July 2022") + public String startDate; + @Schema(example = "10 July 2022") + public String closeDate; @Schema(example = "en") public String locale; + @Schema(example = "dd MMMM yyyy") + public String dateFormat; @Schema(example = "5") public Integer minDepositTerm; @Schema(example = "1") @@ -139,6 +153,10 @@ private PutFixedDepositProductsChanges() {} @Schema(example = "Fixed deposit product new offerings") public String description; + @Schema(example = "10 July 2022") + public String startDate; + @Schema(example = "10 July 2022") + public String closeDate; @Schema(example = "5") public Integer minDepositTerm; } @@ -265,6 +283,12 @@ private GetFixedDepositProductsAccountingRule() {} public String shortName; @Schema(example = "FD01") public String description; + @Schema(example = "[2013, 9, 2]") + public LocalDate startDate; + @Schema(example = "[2014, 2, 7]") + public LocalDate closeDate; + @Schema(example = "loanProduct.active") + public String status; public GetFixedDepositProductsCurrency currency; @Schema(example = "false") public Boolean preClosurePenalApplicable; @@ -491,6 +515,12 @@ private GetFixedDepositProductsProductIdPeriodType() {} public String shortName; @Schema(example = "Daily compounding using Daily Balance, 5% per year, 365 days in year") public String description; + @Schema(example = "[2013, 9, 2]") + public LocalDate startDate; + @Schema(example = "[2014, 2, 7]") + public LocalDate closeDate; + @Schema(example = "loanProduct.active") + public String status; public GetFixedDepositProductsProductIdCurrency currency; public GetFixedDepositProductsProductIdInterestCompoundingPeriodType interestCompoundingPeriodType; public GetFixedDepositProductsResponse.GetFixedDepositProductsInterestPostingPeriodType interestPostingPeriodType; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResource.java index 4347c2fc677..6a3234f561f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResource.java @@ -117,7 +117,7 @@ public class RecurringDepositProductsApiResource { @Operation(summary = "Create a Recurring Deposit Product", operationId = "createRecurringDepositProduct", description = "Creates a Recurring Deposit Product\n\n" + "Mandatory Fields: name, shortName, description, currencyCode, digitsAfterDecimal,inMultiplesOf, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minDepositTerm, minDepositTermTypeId, accountingRule, depositAmount, charts\n\n" + "Mandatory Fields for Cash based accounting (accountingRule = 2): savingsReferenceAccountId, savingsControlAccountId, interestOnSavingsAccountId, incomeFromFeeAccountId, transfersInSuspenseAccountId, incomeFromPenaltyAccountId\n\n" - + "Optional Fields: lockinPeriodFrequency, lockinPeriodFrequencyType, maxDepositTerm, maxDepositTermTypeId, inMultiplesOfDepositTerm, inMultiplesOfDepositTermTypeId, preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnTypeId, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, charges, minDepositAmount, maxDepositAmount, withHoldTax, taxGroupId") + + "Optional Fields: startDate, closeDate, lockinPeriodFrequency, lockinPeriodFrequencyType, maxDepositTerm, maxDepositTermTypeId, inMultiplesOfDepositTerm, inMultiplesOfDepositTermTypeId, preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnTypeId, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, charges, minDepositAmount, maxDepositAmount, withHoldTax, taxGroupId") @RequestBody(required = true, content = @Content(schema = @Schema(implementation = RecurringDepositProductsApiResourceSwagger.PostRecurringDepositProductsRequest.class))) @ApiResponses({ @ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = RecurringDepositProductsApiResourceSwagger.PostRecurringDepositProductsResponse.class))) }) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResourceSwagger.java index 4dc13ceb0e9..e0bed9732c5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/RecurringDepositProductsApiResourceSwagger.java @@ -69,6 +69,10 @@ private PostRecurringDepositProductsChartSlabs() {} public String shortName; @Schema(example = "Daily compounding using Daily Balance, 5% per year, 365 days in year") public String description; + @Schema(example = "10 July 2022") + public String startDate; + @Schema(example = "10 July 2022") + public String closeDate; @Schema(example = "USD") public String currencyCode; @Schema(example = "2") @@ -77,6 +81,8 @@ private PostRecurringDepositProductsChartSlabs() {} public Integer inMultiplesOf; @Schema(example = "en") public String locale; + @Schema(example = "dd MMMM yyyy") + public String dateFormat; @Schema(example = "1") public Integer interestCompoundingPeriodType; @Schema(example = "4") @@ -126,8 +132,14 @@ private PutRecurringDepositProductsRequest() {} @Schema(example = "Recurring deposit product new offerings") public String description; + @Schema(example = "10 July 2022") + public String startDate; + @Schema(example = "10 July 2022") + public String closeDate; @Schema(example = "en") public String locale; + @Schema(example = "dd MMMM yyyy") + public String dateFormat; @Schema(example = "5") public Integer minDepositTerm; @Schema(example = "1") @@ -145,6 +157,10 @@ private PutRecurringDepositProductsChanges() {} @Schema(example = "Recurring deposit product new offerings") public String description; + @Schema(example = "10 July 2022") + public String startDate; + @Schema(example = "10 July 2022") + public String closeDate; @Schema(example = "5") public Integer minDepositTerm; } @@ -283,6 +299,12 @@ private GetRecurringDepositProductsRecurringDepositFrequencyType() {} public String shortName; @Schema(example = "RD01") public String description; + @Schema(example = "[2013, 9, 2]") + public LocalDate startDate; + @Schema(example = "[2014, 2, 7]") + public LocalDate closeDate; + @Schema(example = "loanProduct.active") + public String status; public GetRecurringDepositProductsCurrency currency; @Schema(example = "1") public Integer recurringDepositFrequency; @@ -511,6 +533,12 @@ private GetRecurringDepositProductsProductIdPeriodType() {} public String shortName; @Schema(example = "Daily compounding using Daily Balance, 5% per year, 365 days in year") public String description; + @Schema(example = "[2013, 9, 2]") + public LocalDate startDate; + @Schema(example = "[2014, 2, 7]") + public LocalDate closeDate; + @Schema(example = "loanProduct.active") + public String status; public GetRecurringDepositProductsProductIdCurrency currency; public GetRecurringDepositProductsProductIdInterestCompoundingPeriodType interestCompoundingPeriodType; public GetRecurringDepositProductsResponse.GetRecurringDepositProductsInterestPostingPeriodType interestPostingPeriodType; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductDataValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductDataValidator.java index 33069eae69f..e1c848e7621 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductDataValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductDataValidator.java @@ -145,7 +145,9 @@ public void validateForFixedDepositUpdate(final String json) { validateDepositTermDetailForUpdate(element, baseDataValidator); - validateChartsData(element, baseDataValidator); + if (fromApiJsonHelper.parameterExists(chartsParamName, element)) { + validateChartsData(element, baseDataValidator); + } validateDepositAmountForUpdate(element, baseDataValidator); @@ -202,7 +204,9 @@ public void validateForRecurringDepositUpdate(final String json) { validateRecurringDepositUpdate(element, baseDataValidator); - validateChartsData(element, baseDataValidator); + if (fromApiJsonHelper.parameterExists(chartsParamName, element)) { + validateChartsData(element, baseDataValidator); + } validateDepositAmountForUpdate(element, baseDataValidator); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java index 80af0549f00..be182b082c1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java @@ -70,6 +70,7 @@ import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.staff.domain.Staff; @@ -96,6 +97,7 @@ import org.apache.fineract.portfolio.savings.SavingsPeriodFrequencyType; import org.apache.fineract.portfolio.savings.SavingsPostingInterestPeriodType; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; +import org.apache.fineract.portfolio.savings.exception.DepositAccountApplicationDateException; import org.apache.fineract.portfolio.savings.exception.FixedDepositProductNotFoundException; import org.apache.fineract.portfolio.savings.exception.RecurringDepositProductNotFoundException; import org.apache.fineract.portfolio.savings.exception.SavingsProductNotFoundException; @@ -223,6 +225,7 @@ public SavingsAccount assembleFrom(final JsonCommand command, final AppUser subm } final LocalDate submittedOnDate = this.fromApiJsonHelper.extractLocalDateNamed(submittedOnDateParamName, element); + validateProductApplicationDate(submittedOnDate, product); BigDecimal interestRate = null; if (command.parameterExists(nominalAnnualInterestRateParamName)) { @@ -367,6 +370,22 @@ public SavingsAccount assembleFrom(final JsonCommand command, final AppUser subm return account; } + public void validateProductApplicationDate(final LocalDate submittedOnDate, final SavingsProduct savingsProduct) { + final FixedDepositProduct product = (FixedDepositProduct) savingsProduct; + final LocalDate startDate = product.getStartDate(); + final LocalDate closeDate = product.getCloseDate(); + + if (startDate != null && DateUtils.isBefore(submittedOnDate, startDate)) { + throw new DepositAccountApplicationDateException("submitted.on.date.cannot.be.before.the.deposit.product.start.date", + "submittedOnDate cannot be before the deposit product startDate.", submittedOnDate.toString(), startDate.toString()); + } + + if (closeDate != null && DateUtils.isAfter(submittedOnDate, closeDate)) { + throw new DepositAccountApplicationDateException("submitted.on.date.cannot.be.after.the.deposit.product.close.date", + "submittedOnDate cannot be after the deposit product closeDate.", submittedOnDate.toString(), closeDate.toString()); + } + } + public SavingsAccount assembleFrom(final Long savingsId, DepositAccountType depositAccountType) { final SavingsAccount account = this.savingsAccountRepository.findOneWithNotFoundDetection(savingsId, depositAccountType); account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper, this.configurationDomainService); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java index 3d658c8453b..4d8cf3b1547 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountDomainServiceJpa.java @@ -202,6 +202,7 @@ public Long handleFDAccountClosure(final FixedDepositAccount account, final Paym if (onClosureType == DepositAccountOnClosureType.REINVEST_PRINCIPAL_AND_INTEREST || onClosureType == DepositAccountOnClosureType.REINVEST_PRINCIPAL_ONLY) { ExternalId externalId = this.externalIdFactory.create(); + this.depositAccountAssembler.validateProductApplicationDate(closedDate, account.savingsProduct()); FixedDepositAccount reinvestedDeposit = account.reInvest(account.getAccountBalance(), externalId); this.depositAccountAssembler.assignSavingAccountHelpers(reinvestedDeposit); reinvestedDeposit.updateMaturityDateAndAmountBeforeAccountActivation(mc, isPreMatureClosure, @@ -271,6 +272,7 @@ public Long handleFDAccountMaturityClosure(final FixedDepositAccount account, fi } ExternalId externalId = this.externalIdFactory.create(); + this.depositAccountAssembler.validateProductApplicationDate(closedDate, account.savingsProduct()); FixedDepositAccount reinvestedDeposit = account.reInvest(reInvestAmount, externalId); this.depositAccountAssembler.assignSavingAccountHelpers(reinvestedDeposit); reinvestedDeposit.updateMaturityDateAndAmountBeforeAccountActivation(mc, isPreMatureClosure, @@ -352,6 +354,7 @@ public Long handleRDAccountClosure(final RecurringDepositAccount account, final } else { reInvestAmount = account.getAccountBalance(); } + this.depositAccountAssembler.validateProductApplicationDate(closedDate, account.savingsProduct()); RecurringDepositAccount reinvestedDeposit = account.reInvest(reInvestAmount); depositAccountAssembler.assignSavingAccountHelpers(reinvestedDeposit); this.savingsAccountRepository.save(reinvestedDeposit); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java index 9b8319d9a16..ca2a575f513 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositProductReadPlatformServiceImpl.java @@ -21,11 +21,14 @@ import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; +import java.time.LocalDate; import java.util.Collection; import lombok.RequiredArgsConstructor; import org.apache.fineract.accounting.common.AccountingEnumerations; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.interestratechart.data.InterestRateChartData; @@ -49,6 +52,7 @@ public class DepositProductReadPlatformServiceImpl implements DepositProductRead private final PlatformSecurityContext context; private final JdbcTemplate jdbcTemplate; private final InterestRateChartReadService chartReadPlatformService; + private final DatabaseSpecificSQLGenerator databaseSpecificSQLGenerator; @Override public Collection retrieveAll(final DepositAccountType depositAccountType) { @@ -74,6 +78,9 @@ public Collection retrieveAllForLookup(final DepositAccountT sqlBuilder.append("select "); sqlBuilder.append(DEPOSIT_PRODUCT_LOOKUP_MAPPER.schema()); sqlBuilder.append(" where sp.deposit_type_enum = ? "); + sqlBuilder.append(" and (sp.close_date is null or sp.close_date >= "); + sqlBuilder.append(this.databaseSpecificSQLGenerator.currentBusinessDate()); + sqlBuilder.append(") "); return this.jdbcTemplate.query(sqlBuilder.toString(), DEPOSIT_PRODUCT_LOOKUP_MAPPER, depositAccountType.getValue()); } @@ -144,6 +151,7 @@ protected DepositProductMapper() { sqlBuilder.append("sp.lockin_period_frequency_enum as lockinPeriodFrequencyType, "); sqlBuilder.append("sp.accounting_type as accountingType, "); sqlBuilder.append("sp.min_balance_for_interest_calculation as minBalanceForInterestCalculation, "); + sqlBuilder.append("sp.start_date as startDate, sp.close_date as closeDate, "); sqlBuilder.append("sp.withhold_tax as withHoldTax,"); sqlBuilder.append("tg.id as taxGroupId, tg.name as taxGroupName "); this.schemaSql = sqlBuilder.toString(); @@ -197,6 +205,10 @@ public DepositProductData mapRow(final ResultSet rs) throws SQLException { lockinPeriodFrequencyType = SavingsEnumerations.lockinPeriodFrequencyType(lockinPeriodFrequencyTypeValue); } final BigDecimal minBalanceForInterestCalculation = rs.getBigDecimal("minBalanceForInterestCalculation"); + final LocalDate startDate = JdbcSupport.getLocalDate(rs, "startDate"); + final LocalDate closeDate = JdbcSupport.getLocalDate(rs, "closeDate"); + final String status = closeDate != null && DateUtils.isBeforeBusinessDate(closeDate) ? "loanProduct.inActive" + : "loanProduct.active"; final boolean withHoldTax = rs.getBoolean("withHoldTax"); final Long taxGroupId = JdbcSupport.getLong(rs, "taxGroupId"); @@ -209,7 +221,7 @@ public DepositProductData mapRow(final ResultSet rs) throws SQLException { return DepositProductData.instance(id, name, shortName, description, currency, nominalAnnualInterestRate, compoundingInterestPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, lockinPeriodFrequency, lockinPeriodFrequencyType, accountingRuleType, minBalanceForInterestCalculation, withHoldTax, - taxGroupData); + taxGroupData, startDate, closeDate, status); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java index 1c21b4c2ed5..6d52e827d2d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/starter/SavingsConfiguration.java @@ -269,8 +269,9 @@ public DepositApplicationProcessWritePlatformService depositApplicationProcessWr @Bean @ConditionalOnMissingBean(DepositProductReadPlatformService.class) public DepositProductReadPlatformService depositProductReadPlatformService(PlatformSecurityContext context, JdbcTemplate jdbcTemplate, - InterestRateChartReadService interestRateChartReadPlatformService) { - return new DepositProductReadPlatformServiceImpl(context, jdbcTemplate, interestRateChartReadPlatformService); + InterestRateChartReadService interestRateChartReadPlatformService, DatabaseSpecificSQLGenerator databaseSpecificSQLGenerator) { + return new DepositProductReadPlatformServiceImpl(context, jdbcTemplate, interestRateChartReadPlatformService, + databaseSpecificSQLGenerator); } @Bean diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 339750468cd..2a0d0fefdb2 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -255,4 +255,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0237_add_deposit_product_validity_dates.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0237_add_deposit_product_validity_dates.xml new file mode 100644 index 00000000000..e05fcc25f90 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0237_add_deposit_product_validity_dates.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductData.java index 04c72bf02ff..a753f9684a9 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductData.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/DepositProductData.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.savings.data; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.Collection; import java.util.List; import java.util.Map; @@ -54,6 +55,9 @@ public class DepositProductData { // protected final boolean withdrawalFeeForTransfers; protected final BigDecimal minBalanceForInterestCalculation; protected final boolean withHoldTax; + protected final LocalDate startDate; + protected final LocalDate closeDate; + protected final String status; protected final TaxGroupData taxGroup; // accounting @@ -115,6 +119,9 @@ public static DepositProductData template(final CurrencyData currency, final Enu final Collection interestRateCharts = null; final boolean withHoldTax = false; final TaxGroupData taxGroup = null; + final LocalDate startDate = null; + final LocalDate closeDate = null; + final String status = null; return new DepositProductData(id, name, shortName, description, currency, nominalAnnualInterestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, lockinPeriodFrequency, @@ -123,7 +130,7 @@ public static DepositProductData template(final CurrencyData currency, final Enu interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions, paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, charges, chargeOptions, penaltyOptions, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, interestRateCharts, chartTemplate, minBalanceForInterestCalculation, withHoldTax, taxGroup, - taxGroupOptions); + taxGroupOptions, startDate, closeDate, status); } public static DepositProductData withCharges(final DepositProductData existingProduct, final Collection charges) { @@ -140,7 +147,7 @@ public static DepositProductData withCharges(final DepositProductData existingPr charges, existingProduct.chargeOptions, existingProduct.penaltyOptions, existingProduct.feeToIncomeAccountMappings, existingProduct.penaltyToIncomeAccountMappings, existingProduct.interestRateCharts, existingProduct.chartTemplate, existingProduct.minBalanceForInterestCalculation, existingProduct.withHoldTax, existingProduct.taxGroup, - existingProduct.taxGroupOptions); + existingProduct.taxGroupOptions, existingProduct.startDate, existingProduct.closeDate, existingProduct.status); } /** @@ -172,7 +179,8 @@ public static DepositProductData withTemplate(final DepositProductData existingP accountingMappingOptions, existingProduct.charges, chargeOptions, penaltyOptions, existingProduct.feeToIncomeAccountMappings, existingProduct.penaltyToIncomeAccountMappings, existingProduct.interestRateCharts, chartTemplate, existingProduct.minBalanceForInterestCalculation, - existingProduct.withHoldTax, existingProduct.taxGroup, taxGroupOptions); + existingProduct.withHoldTax, existingProduct.taxGroup, taxGroupOptions, existingProduct.startDate, + existingProduct.closeDate, existingProduct.status); } public static DepositProductData withAccountingDetails(final DepositProductData existingProduct, @@ -204,7 +212,8 @@ public static DepositProductData withAccountingDetails(final DepositProductData withdrawalFeeTypeOptions, paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, existingProduct.charges, chargeOptions, penaltyOptions, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, existingProduct.interestRateCharts, existingProduct.chartTemplate, existingProduct.minBalanceForInterestCalculation, - existingProduct.withHoldTax, existingProduct.taxGroup, taxGroupOptions); + existingProduct.withHoldTax, existingProduct.taxGroup, taxGroupOptions, existingProduct.startDate, + existingProduct.closeDate, existingProduct.status); } public static DepositProductData instance(final Long id, final String name, final String shortName, final String description, @@ -212,7 +221,8 @@ public static DepositProductData instance(final Long id, final String name, fina final EnumOptionData interestPostingPeriodType, final EnumOptionData interestCalculationType, final EnumOptionData interestCalculationDaysInYearType, final Integer lockinPeriodFrequency, final EnumOptionData lockinPeriodFrequencyType, final EnumOptionData accountingType, - final BigDecimal minBalanceForInterestCalculation, boolean withHoldTax, TaxGroupData taxGroup) { + final BigDecimal minBalanceForInterestCalculation, boolean withHoldTax, TaxGroupData taxGroup, final LocalDate startDate, + final LocalDate closeDate, final String status) { final Map accountingMappings = null; final Collection paymentChannelToFundSourceMappings = null; @@ -243,7 +253,7 @@ public static DepositProductData instance(final Long id, final String name, fina interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions, paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, charges, chargeOptions, penaltyOptions, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, interestRateCharts, chartTemplate, minBalanceForInterestCalculation, withHoldTax, taxGroup, - taxGroupOptions); + taxGroupOptions, startDate, closeDate, status); } public static DepositProductData lookup(final Long id, final String name) { @@ -259,6 +269,9 @@ public static DepositProductData lookup(final Long id, final String name) { final Integer lockinPeriodFrequency = null; final EnumOptionData lockinPeriodFrequencyType = null; final BigDecimal minBalanceForInterestCalculation = null; + final LocalDate startDate = null; + final LocalDate closeDate = null; + final String status = null; final EnumOptionData accountingType = null; final Map accountingMappings = null; @@ -292,7 +305,7 @@ public static DepositProductData lookup(final Long id, final String name) { interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions, paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, charges, chargeOptions, penaltyOptions, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, interestRateCharts, chartTemplate, minBalanceForInterestCalculation, withHoldTax, taxGroup, - taxGroupOptions); + taxGroupOptions, startDate, closeDate, status); } public static DepositProductData withInterestChart(final DepositProductData existingProduct, @@ -310,7 +323,8 @@ public static DepositProductData withInterestChart(final DepositProductData exis existingProduct.charges, existingProduct.chargeOptions, existingProduct.penaltyOptions, existingProduct.feeToIncomeAccountMappings, existingProduct.penaltyToIncomeAccountMappings, interestRateCharts, existingProduct.chartTemplate, existingProduct.minBalanceForInterestCalculation, existingProduct.withHoldTax, - existingProduct.taxGroup, existingProduct.taxGroupOptions); + existingProduct.taxGroup, existingProduct.taxGroupOptions, existingProduct.startDate, existingProduct.closeDate, + existingProduct.status); } protected DepositProductData(final Long id, final String name, final String shortName, final String description, @@ -331,7 +345,7 @@ protected DepositProductData(final Long id, final String name, final String shor final Collection penaltyToIncomeAccountMappings, final Collection interestRateCharts, final InterestRateChartData chartTemplate, final BigDecimal minBalanceForInterestCalculation, final boolean withHoldTax, final TaxGroupData taxGroup, - final Collection taxGroupOptions) { + final Collection taxGroupOptions, final LocalDate startDate, final LocalDate closeDate, final String status) { this.id = id; this.name = name; @@ -380,6 +394,9 @@ protected DepositProductData(final Long id, final String name, final String shor this.taxGroup = taxGroup; this.withHoldTax = withHoldTax; this.taxGroupOptions = taxGroupOptions; + this.startDate = startDate; + this.closeDate = closeDate; + this.status = status; } public static InterestRateChartData activeChart(Collection interestRateCharts) { diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositProductData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositProductData.java index 2c61643e1fb..ef3266c405f 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositProductData.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/FixedDepositProductData.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.savings.data; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.Collection; import java.util.List; import java.util.Map; @@ -108,7 +109,7 @@ public static FixedDepositProductData template(final CurrencyData currency, fina preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnType, preClosurePenalInterestOnTypeOptions, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, minDepositAmount, depositAmount, maxDepositAmount, periodFrequencyTypeOptions, withHoldTax, - taxGroup, taxGroupOptions); + taxGroup, taxGroupOptions, null, null, null); } public static FixedDepositProductData withCharges(final FixedDepositProductData existingProduct, final Collection charges) { @@ -130,7 +131,7 @@ public static FixedDepositProductData withCharges(final FixedDepositProductData existingProduct.maxDepositTermType, existingProduct.inMultiplesOfDepositTerm, existingProduct.inMultiplesOfDepositTermType, existingProduct.minDepositAmount, existingProduct.depositAmount, existingProduct.maxDepositAmount, existingProduct.periodFrequencyTypeOptions, existingProduct.withHoldTax, existingProduct.taxGroup, - existingProduct.taxGroupOptions); + existingProduct.taxGroupOptions, existingProduct.startDate, existingProduct.closeDate, existingProduct.status); } /** @@ -168,7 +169,7 @@ public static FixedDepositProductData withTemplate(final FixedDepositProductData existingProduct.minDepositTermType, existingProduct.maxDepositTermType, existingProduct.inMultiplesOfDepositTerm, existingProduct.inMultiplesOfDepositTermType, existingProduct.minDepositAmount, existingProduct.depositAmount, existingProduct.maxDepositAmount, periodFrequencyTypeOptions, existingProduct.withHoldTax, existingProduct.taxGroup, - taxGroupOptions); + taxGroupOptions, existingProduct.startDate, existingProduct.closeDate, existingProduct.status); } public static FixedDepositProductData withAccountingDetails(final FixedDepositProductData existingProduct, @@ -205,7 +206,7 @@ public static FixedDepositProductData withAccountingDetails(final FixedDepositPr existingProduct.maxDepositTermType, existingProduct.inMultiplesOfDepositTerm, existingProduct.inMultiplesOfDepositTermType, existingProduct.minDepositAmount, existingProduct.depositAmount, existingProduct.maxDepositAmount, existingProduct.periodFrequencyTypeOptions, existingProduct.withHoldTax, existingProduct.taxGroup, - existingProduct.taxGroupOptions); + existingProduct.taxGroupOptions, existingProduct.startDate, existingProduct.closeDate, existingProduct.status); } public static FixedDepositProductData instance(final DepositProductData depositProductData, final boolean preClosurePenalApplicable, @@ -251,7 +252,8 @@ public static FixedDepositProductData instance(final DepositProductData depositP preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnType, preClosurePenalInterestOnTypeOptions, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, minDepositAmount, depositAmount, maxDepositAmount, periodFrequencyTypeOptions, - depositProductData.withHoldTax, depositProductData.taxGroup, taxGroupOptions); + depositProductData.withHoldTax, depositProductData.taxGroup, taxGroupOptions, depositProductData.startDate, + depositProductData.closeDate, depositProductData.status); } public static FixedDepositProductData lookup(final Long id, final String name) { @@ -316,7 +318,7 @@ public static FixedDepositProductData lookup(final Long id, final String name) { preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnType, preClosurePenalInterestOnTypeOptions, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, minDepositAmount, depositAmount, maxDepositAmount, periodFrequencyTypeOptions, withHoldTax, - taxGroup, taxGroupOptions); + taxGroup, taxGroupOptions, null, null, null); } public static FixedDepositProductData withInterestChart(final FixedDepositProductData existingProduct, @@ -339,7 +341,7 @@ public static FixedDepositProductData withInterestChart(final FixedDepositProduc existingProduct.maxDepositTermType, existingProduct.inMultiplesOfDepositTerm, existingProduct.inMultiplesOfDepositTermType, existingProduct.minDepositAmount, existingProduct.depositAmount, existingProduct.maxDepositAmount, existingProduct.periodFrequencyTypeOptions, existingProduct.withHoldTax, existingProduct.taxGroup, - existingProduct.taxGroupOptions); + existingProduct.taxGroupOptions, existingProduct.startDate, existingProduct.closeDate, existingProduct.status); } @@ -367,7 +369,8 @@ private FixedDepositProductData(final Long id, final String name, final String s final EnumOptionData maxDepositTermType, final Integer inMultiplesOfDepositTerm, final EnumOptionData inMultiplesOfDepositTermType, final BigDecimal minDepositAmount, final BigDecimal depositAmount, final BigDecimal maxDepositAmount, final Collection periodFrequencyTypeOptions, final boolean withHoldTax, - final TaxGroupData taxGroup, final Collection taxGroupOptions) { + final TaxGroupData taxGroup, final Collection taxGroupOptions, final LocalDate startDate, + final LocalDate closeDate, final String status) { super(id, name, shortName, description, currency, nominalAnnualInterestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, lockinPeriodFrequency, @@ -376,7 +379,7 @@ private FixedDepositProductData(final Long id, final String name, final String s interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions, paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, charges, chargeOptions, penaltyOptions, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, interestRateCharts, chartTemplate, minBalanceForInterestCalculation, withHoldTax, taxGroup, - taxGroupOptions); + taxGroupOptions, startDate, closeDate, status); // fixed deposit additional fields this.preClosurePenalApplicable = preClosurePenalApplicable; diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java index 7952a014471..3d8567ecebd 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/data/RecurringDepositProductData.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.savings.data; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.Collection; import java.util.List; import java.util.Map; @@ -114,7 +115,8 @@ public static RecurringDepositProductData template(final CurrencyData currency, preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnType, preClosurePenalInterestOnTypeOptions, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, isMandatoryDeposit, allowWithdrawal, adjustAdvanceTowardsFuturePayments, - periodFrequencyTypeOptions, minDepositAmount, depositAmount, maxDepositAmount, withHoldTax, taxGroup, taxGroupOptions); + periodFrequencyTypeOptions, minDepositAmount, depositAmount, maxDepositAmount, withHoldTax, taxGroup, taxGroupOptions, null, + null, null); } public static RecurringDepositProductData withCharges(final RecurringDepositProductData existingProduct, @@ -138,7 +140,8 @@ public static RecurringDepositProductData withCharges(final RecurringDepositProd existingProduct.maxDepositTermType, existingProduct.inMultiplesOfDepositTerm, existingProduct.inMultiplesOfDepositTermType, existingProduct.isMandatoryDeposit, existingProduct.allowWithdrawal, existingProduct.adjustAdvanceTowardsFuturePayments, existingProduct.periodFrequencyTypeOptions, existingProduct.minDepositAmount, existingProduct.depositAmount, - existingProduct.maxDepositAmount, existingProduct.withHoldTax, existingProduct.taxGroup, existingProduct.taxGroupOptions); + existingProduct.maxDepositAmount, existingProduct.withHoldTax, existingProduct.taxGroup, existingProduct.taxGroupOptions, + existingProduct.startDate, existingProduct.closeDate, existingProduct.status); } /** @@ -178,7 +181,7 @@ public static RecurringDepositProductData withTemplate(final RecurringDepositPro existingProduct.inMultiplesOfDepositTermType, existingProduct.isMandatoryDeposit, existingProduct.allowWithdrawal, existingProduct.adjustAdvanceTowardsFuturePayments, periodFrequencyTypeOptions, existingProduct.minDepositAmount, existingProduct.depositAmount, existingProduct.maxDepositAmount, existingProduct.withHoldTax, existingProduct.taxGroup, - taxGroupOptions); + taxGroupOptions, existingProduct.startDate, existingProduct.closeDate, existingProduct.status); } @@ -217,7 +220,8 @@ public static RecurringDepositProductData withAccountingDetails(final RecurringD existingProduct.inMultiplesOfDepositTermType, existingProduct.isMandatoryDeposit, existingProduct.allowWithdrawal, existingProduct.adjustAdvanceTowardsFuturePayments, existingProduct.periodFrequencyTypeOptions, existingProduct.minDepositAmount, existingProduct.depositAmount, existingProduct.maxDepositAmount, - existingProduct.withHoldTax, existingProduct.taxGroup, existingProduct.taxGroupOptions); + existingProduct.withHoldTax, existingProduct.taxGroup, existingProduct.taxGroupOptions, existingProduct.startDate, + existingProduct.closeDate, existingProduct.status); } public static RecurringDepositProductData instance(final DepositProductData depositProductData, final boolean preClosurePenalApplicable, @@ -265,7 +269,8 @@ public static RecurringDepositProductData instance(final DepositProductData depo minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, isMandatoryDeposit, allowWithdrawal, adjustAdvanceTowardsFuturePayments, periodFrequencyTypeOptions, minDepositAmount, depositAmount, maxDepositAmount, depositProductData.withHoldTax, - depositProductData.taxGroup, taxGroupOptions); + depositProductData.taxGroup, taxGroupOptions, depositProductData.startDate, depositProductData.closeDate, + depositProductData.status); } public static RecurringDepositProductData lookup(final Long id, final String name) { @@ -335,7 +340,8 @@ public static RecurringDepositProductData lookup(final Long id, final String nam preClosurePenalApplicable, preClosurePenalInterest, preClosurePenalInterestOnType, preClosurePenalInterestOnTypeOptions, minDepositTerm, maxDepositTerm, minDepositTermType, maxDepositTermType, inMultiplesOfDepositTerm, inMultiplesOfDepositTermType, isMandatoryDeposit, allowWithdrawal, adjustAdvanceTowardsFuturePayments, - periodFrequencyTypeOptions, minDepositAmount, depositAmount, maxDepositAmount, withHoldTax, taxGroup, taxGroupOptions); + periodFrequencyTypeOptions, minDepositAmount, depositAmount, maxDepositAmount, withHoldTax, taxGroup, taxGroupOptions, null, + null, null); } public static RecurringDepositProductData withInterestChart(final RecurringDepositProductData product, @@ -355,7 +361,8 @@ public static RecurringDepositProductData withInterestChart(final RecurringDepos product.maxDepositTerm, product.minDepositTermType, product.maxDepositTermType, product.inMultiplesOfDepositTerm, product.inMultiplesOfDepositTermType, product.isMandatoryDeposit, product.allowWithdrawal, product.adjustAdvanceTowardsFuturePayments, product.periodFrequencyTypeOptions, product.minDepositAmount, - product.depositAmount, product.maxDepositAmount, product.withHoldTax, product.taxGroup, product.taxGroupOptions); + product.depositAmount, product.maxDepositAmount, product.withHoldTax, product.taxGroup, product.taxGroupOptions, + product.startDate, product.closeDate, product.status); } @@ -384,7 +391,8 @@ private RecurringDepositProductData(final Long id, final String name, final Stri final EnumOptionData inMultiplesOfDepositTermType, final boolean isMandatoryDeposit, boolean allowWithdrawal, boolean adjustAdvanceTowardsFuturePayments, final Collection periodFrequencyTypeOptions, final BigDecimal minDepositAmount, final BigDecimal depositAmount, final BigDecimal maxDepositAmount, final boolean withHoldTax, - final TaxGroupData taxGroup, final Collection taxGroupOptions) { + final TaxGroupData taxGroup, final Collection taxGroupOptions, final LocalDate startDate, + final LocalDate closeDate, final String status) { super(id, name, shortName, description, currency, nominalAnnualInterestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, lockinPeriodFrequency, @@ -393,7 +401,7 @@ private RecurringDepositProductData(final Long id, final String name, final Stri interestCalculationDaysInYearTypeOptions, lockinPeriodFrequencyTypeOptions, withdrawalFeeTypeOptions, paymentTypeOptions, accountingRuleOptions, accountingMappingOptions, charges, chargeOptions, penaltyOptions, feeToIncomeAccountMappings, penaltyToIncomeAccountMappings, interestRateCharts, chartTemplate, minBalanceForInterestCalculation, withHoldTax, taxGroup, - taxGroupOptions); + taxGroupOptions, startDate, closeDate, status); this.preClosurePenalApplicable = preClosurePenalApplicable; this.preClosurePenalInterest = preClosurePenalInterest; diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositProductAssembler.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositProductAssembler.java index 5d4a29d347c..408a2d935d6 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositProductAssembler.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositProductAssembler.java @@ -23,6 +23,7 @@ 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.chartsParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.closeDateParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositAmountParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositMaxAmountParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.depositMinAmountParamName; @@ -36,6 +37,7 @@ 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.startDateParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.chargesParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.currencyCodeParamName; import static org.apache.fineract.portfolio.savings.SavingsApiConstants.descriptionParamName; @@ -58,6 +60,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -105,6 +108,8 @@ public FixedDepositProduct assembleFixedDepositProduct(final JsonCommand command final String name = command.stringValueOfParameterNamed(nameParamName); final String shortName = command.stringValueOfParameterNamed(shortNameParamName); final String description = command.stringValueOfParameterNamed(descriptionParamName); + final LocalDate startDate = command.localDateValueOfParameterNamed(startDateParamName); + final LocalDate closeDate = command.localDateValueOfParameterNamed(closeDateParamName); final String currencyCode = command.stringValueOfParameterNamed(currencyCodeParamName); final Integer digitsAfterDecimal = command.integerValueOfParameterNamed(digitsAfterDecimalParamName); @@ -178,7 +183,7 @@ public FixedDepositProduct assembleFixedDepositProduct(final JsonCommand command FixedDepositProduct fixedDepositProduct = FixedDepositProduct.createNew(name, shortName, description, currency, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, lockinPeriodFrequency, lockinPeriodFrequencyType, accountingRuleType, charges, productTermAndPreClosure, charts, - minBalanceForInterestCalculation, withHoldTax, taxGroup); + minBalanceForInterestCalculation, withHoldTax, taxGroup, startDate, closeDate); // update product reference productTermAndPreClosure.updateProductReference(fixedDepositProduct); @@ -199,6 +204,8 @@ public RecurringDepositProduct assembleRecurringDepositProduct(final JsonCommand final String name = command.stringValueOfParameterNamed(nameParamName); final String shortName = command.stringValueOfParameterNamed(shortNameParamName); final String description = command.stringValueOfParameterNamed(descriptionParamName); + final LocalDate startDate = command.localDateValueOfParameterNamed(startDateParamName); + final LocalDate closeDate = command.localDateValueOfParameterNamed(closeDateParamName); final String currencyCode = command.stringValueOfParameterNamed(currencyCodeParamName); final Integer digitsAfterDecimal = command.integerValueOfParameterNamed(digitsAfterDecimalParamName); @@ -271,7 +278,8 @@ public RecurringDepositProduct assembleRecurringDepositProduct(final JsonCommand RecurringDepositProduct recurringDepositProduct = RecurringDepositProduct.createNew(name, shortName, description, currency, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, lockinPeriodFrequency, lockinPeriodFrequencyType, accountingRuleType, charges, - productTermAndPreClosure, productRecurringDetail, charts, minBalanceForInterestCalculation, taxGroup, withHoldTax); + productTermAndPreClosure, productRecurringDetail, charts, minBalanceForInterestCalculation, taxGroup, withHoldTax, + startDate, closeDate); // update product reference productTermAndPreClosure.updateProductReference(recurringDepositProduct); diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositProduct.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositProduct.java index 40519c2e5ef..f47dbe886e0 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositProduct.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositProduct.java @@ -21,11 +21,16 @@ import static org.apache.fineract.portfolio.interestratechart.InterestRateChartApiConstants.deleteParamName; import static org.apache.fineract.portfolio.interestratechart.InterestRateChartApiConstants.idParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.FIXED_DEPOSIT_PRODUCT_RESOURCE_NAME; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.closeDateParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.dateFormatParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.localeParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maxDepositTermParamName; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.startDateParamName; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -44,11 +49,13 @@ import java.util.List; import java.util.Map; import java.util.Set; +import lombok.Getter; import org.apache.fineract.accounting.common.AccountingRuleType; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.interestratechart.InterestRateChartApiConstants; @@ -69,6 +76,14 @@ public class FixedDepositProduct extends SavingsProduct { @OneToOne(mappedBy = "product", cascade = CascadeType.ALL) private DepositProductTermAndPreClosure productTermAndPreClosure; + @Getter + @Column(name = "start_date") + private LocalDate startDate; + + @Getter + @Column(name = "close_date") + private LocalDate closeDate; + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL) @JoinTable(name = "m_deposit_product_interest_rate_chart", joinColumns = @JoinColumn(name = "deposit_product_id"), inverseJoinColumns = @JoinColumn(name = "interest_rate_chart_id", unique = true)) protected Set charts; @@ -87,7 +102,7 @@ public static FixedDepositProduct createNew(final String name, final String shor final SavingsInterestCalculationDaysInYearType interestCalculationDaysInYearType, final Integer lockinPeriodFrequency, final SavingsPeriodFrequencyType lockinPeriodFrequencyType, final AccountingRuleType accountingRuleType, final Set charges, final DepositProductTermAndPreClosure productTermAndPreClosure, final Set charts, - BigDecimal minBalanceForInterestCalculation, boolean withHoldTax, TaxGroup taxGroup) { + BigDecimal minBalanceForInterestCalculation, boolean withHoldTax, TaxGroup taxGroup, LocalDate startDate, LocalDate closeDate) { final BigDecimal minRequiredOpeningBalance = null; final boolean withdrawalFeeApplicableForTransfer = false; @@ -97,7 +112,8 @@ public static FixedDepositProduct createNew(final String name, final String shor return new FixedDepositProduct(name, shortName, description, currency, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, accountingRuleType, charges, - productTermAndPreClosure, charts, allowOverdraft, overdraftLimit, minBalanceForInterestCalculation, withHoldTax, taxGroup); + productTermAndPreClosure, charts, allowOverdraft, overdraftLimit, minBalanceForInterestCalculation, withHoldTax, taxGroup, + startDate, closeDate); } protected FixedDepositProduct(final String name, final String shortName, final String description, final MonetaryCurrency currency, @@ -108,7 +124,7 @@ protected FixedDepositProduct(final String name, final String shortName, final S final boolean withdrawalFeeApplicableForTransfer, final AccountingRuleType accountingRuleType, final Set charges, final DepositProductTermAndPreClosure productTermAndPreClosure, final Set charts, final boolean allowOverdraft, final BigDecimal overdraftLimit, final BigDecimal minBalanceForInterestCalculation, - boolean withHoldTax, TaxGroup taxGroup) { + boolean withHoldTax, TaxGroup taxGroup, LocalDate startDate, LocalDate closeDate) { super(name, shortName, description, currency, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, @@ -119,6 +135,8 @@ protected FixedDepositProduct(final String name, final String shortName, final S this.charts = charts; } + this.startDate = startDate; + this.closeDate = closeDate; this.productTermAndPreClosure = productTermAndPreClosure; } @@ -170,6 +188,20 @@ protected Map update(final JsonCommand command, final DataValida updateCharts(command, actualChanges, baseDataValidator); } + if (command.isChangeInLocalDateParameterNamed(startDateParamName, this.startDate)) { + actualChanges.put(startDateParamName, command.stringValueOfParameterNamed(startDateParamName)); + actualChanges.put(dateFormatParamName, command.dateFormat()); + actualChanges.put(localeParamName, command.locale()); + this.startDate = command.localDateValueOfParameterNamed(startDateParamName); + } + + if (command.isChangeInLocalDateParameterNamed(closeDateParamName, this.closeDate)) { + actualChanges.put(closeDateParamName, command.stringValueOfParameterNamed(closeDateParamName)); + actualChanges.put(dateFormatParamName, command.dateFormat()); + actualChanges.put(localeParamName, command.locale()); + this.closeDate = command.localDateValueOfParameterNamed(closeDateParamName); + } + return actualChanges; } @@ -343,6 +375,11 @@ public void validateDomainRules() { private void validateDomainRules(final DataValidatorBuilder baseDataValidator) { + if (this.startDate != null && this.closeDate != null && DateUtils.isBefore(this.closeDate, this.startDate)) { + baseDataValidator.reset().parameter(closeDateParamName).value(this.closeDate).failWithCode("must.be.after.startDate", + this.closeDate.toString(), this.startDate.toString()); + } + final DepositTermDetail termDetails = this.depositProductTermAndPreClosure().depositTermDetail(); final boolean isMinTermGreaterThanMax = termDetails.isMinDepositTermGreaterThanMaxDepositTerm(); if (isMinTermGreaterThanMax) { diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositProduct.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositProduct.java index ede63a24f31..96400374ded 100644 --- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositProduct.java +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/RecurringDepositProduct.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.savings.domain; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.RECURRING_DEPOSIT_PRODUCT_RESOURCE_NAME; +import static org.apache.fineract.portfolio.savings.DepositsApiConstants.closeDateParamName; import static org.apache.fineract.portfolio.savings.DepositsApiConstants.maxDepositTermParamName; import jakarta.persistence.CascadeType; @@ -26,6 +27,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.OneToOne; import java.math.BigDecimal; +import java.time.LocalDate; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -36,6 +38,7 @@ import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.interestratechart.domain.InterestRateChart; @@ -66,7 +69,8 @@ public static RecurringDepositProduct createNew(final String name, final String final SavingsPeriodFrequencyType lockinPeriodFrequencyType, final AccountingRuleType accountingRuleType, final Set charges, final DepositProductTermAndPreClosure productTermAndPreClosure, final DepositProductRecurringDetail recurringDetail, final Set charts, - BigDecimal minBalanceForInterestCalculation, final TaxGroup taxGroup, final boolean withHoldTax) { + BigDecimal minBalanceForInterestCalculation, final TaxGroup taxGroup, final boolean withHoldTax, LocalDate startDate, + LocalDate closeDate) { final BigDecimal minRequiredOpeningBalance = null; final boolean withdrawalFeeApplicableForTransfer = false; @@ -77,7 +81,7 @@ public static RecurringDepositProduct createNew(final String name, final String interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, accountingRuleType, charges, productTermAndPreClosure, recurringDetail, charts, allowOverdraft, overdraftLimit, minBalanceForInterestCalculation, - withHoldTax, taxGroup); + withHoldTax, taxGroup, startDate, closeDate); } protected RecurringDepositProduct(final String name, final String shortName, final String description, final MonetaryCurrency currency, @@ -88,12 +92,13 @@ protected RecurringDepositProduct(final String name, final String shortName, fin final boolean withdrawalFeeApplicableForTransfer, final AccountingRuleType accountingRuleType, final Set charges, final DepositProductTermAndPreClosure productTermAndPreClosure, final DepositProductRecurringDetail recurringDetail, final Set charts, final boolean allowOverdraft, final BigDecimal overdraftLimit, - final BigDecimal minBalanceForInterestCalculation, final boolean withHoldTax, final TaxGroup taxGroup) { + final BigDecimal minBalanceForInterestCalculation, final boolean withHoldTax, final TaxGroup taxGroup, LocalDate startDate, + LocalDate closeDate) { super(name, shortName, description, currency, interestRate, interestCompoundingPeriodType, interestPostingPeriodType, interestCalculationType, interestCalculationDaysInYearType, minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, withdrawalFeeApplicableForTransfer, accountingRuleType, charges, productTermAndPreClosure, - charts, allowOverdraft, overdraftLimit, minBalanceForInterestCalculation, withHoldTax, taxGroup); + charts, allowOverdraft, overdraftLimit, minBalanceForInterestCalculation, withHoldTax, taxGroup, startDate, closeDate); this.recurringDetail = recurringDetail; } @@ -150,6 +155,11 @@ public void validateDomainRules() { } private void validateDomainRules(final DataValidatorBuilder baseDataValidator) { + if (getStartDate() != null && getCloseDate() != null && DateUtils.isBefore(getCloseDate(), getStartDate())) { + baseDataValidator.reset().parameter(closeDateParamName).value(getCloseDate()).failWithCode("must.be.after.startDate", + getCloseDate().toString(), getStartDate().toString()); + } + final DepositTermDetail termDetails = this.depositProductTermAndPreClosure().depositTermDetail(); final boolean isMinTermGreaterThanMax = termDetails.isMinDepositTermGreaterThanMaxDepositTerm(); if (isMinTermGreaterThanMax) { diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/DepositAccountApplicationDateException.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/DepositAccountApplicationDateException.java new file mode 100644 index 00000000000..15f672ef614 --- /dev/null +++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/exception/DepositAccountApplicationDateException.java @@ -0,0 +1,29 @@ +/** + * 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.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class DepositAccountApplicationDateException extends AbstractPlatformDomainRuleException { + + public DepositAccountApplicationDateException(final String postFix, final String defaultUserMessage, + final Object... defaultUserMessageArgs) { + super("error.msg.deposit.account.application." + postFix, defaultUserMessage, defaultUserMessageArgs); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignDepositProductArchivingTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignDepositProductArchivingTest.java new file mode 100644 index 00000000000..0af61c206b9 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignDepositProductArchivingTest.java @@ -0,0 +1,267 @@ +/** + * 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.client.feign.tests; + +import static org.apache.fineract.client.feign.util.FeignCalls.fail; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import java.time.LocalDate; +import java.util.Map; +import java.util.Set; +import org.apache.fineract.client.feign.FeignException; +import org.apache.fineract.client.feign.ObjectMapperFactory; +import org.apache.fineract.client.feign.util.CallFailedRuntimeException; +import org.apache.fineract.client.models.GetFixedDepositAccountsAccountIdResponse; +import org.apache.fineract.client.models.GetFixedDepositAccountsTemplateResponse; +import org.apache.fineract.client.models.GetFixedDepositProductsProductIdResponse; +import org.apache.fineract.client.models.GetRecurringDepositAccountsAccountIdResponse; +import org.apache.fineract.client.models.GetRecurringDepositAccountsTemplateResponse; +import org.apache.fineract.client.models.GetRecurringDepositProductsProductIdResponse; +import org.apache.fineract.client.models.PostFixedDepositAccountsRequest; +import org.apache.fineract.client.models.PostFixedDepositProductsChartSlabs; +import org.apache.fineract.client.models.PostFixedDepositProductsCharts; +import org.apache.fineract.client.models.PostFixedDepositProductsRequest; +import org.apache.fineract.client.models.PostRecurringDepositAccountsRequest; +import org.apache.fineract.client.models.PostRecurringDepositProductsChartSlabs; +import org.apache.fineract.client.models.PostRecurringDepositProductsCharts; +import org.apache.fineract.client.models.PostRecurringDepositProductsRequest; +import org.apache.fineract.client.models.PutFixedDepositProductsProductIdRequest; +import org.apache.fineract.client.models.PutRecurringDepositProductsRequest; +import org.apache.fineract.integrationtests.client.FeignIntegrationTest; +import org.apache.fineract.integrationtests.client.feign.helpers.FeignBusinessDateHelper; +import org.apache.fineract.integrationtests.client.feign.helpers.FeignClientHelper; +import org.apache.fineract.integrationtests.client.feign.helpers.FeignSchedulerHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.junit.jupiter.api.Test; + +public class FeignDepositProductArchivingTest extends FeignIntegrationTest { + + private static final String INACTIVE_STATUS = "loanProduct.inActive"; + private static final String MATURED_STATUS = "savingsAccountStatusType.matured"; + private static final String BEFORE_START_DATE_ERROR = "error.msg.deposit.account.application.submitted.on.date.cannot.be.before.the.deposit.product.start.date"; + private static final String AFTER_CLOSE_DATE_ERROR = "error.msg.deposit.account.application.submitted.on.date.cannot.be.after.the.deposit.product.close.date"; + + @Test + public void testFixedDepositProductCanBeArchived() throws JsonProcessingException { + final LocalDate businessDate = businessDate(); + final Long productId = createFixedDepositProduct(businessDate.minusMonths(3)); + + final GetFixedDepositAccountsTemplateResponse activeTemplate = ok( + () -> fineractClient().fixedDepositAccount().retrieveTemplateFixedDepositAccount(null, null, null, false)); + assertThat(activeTemplate.getProductOptions().stream().anyMatch(product -> productId.equals(product.getId()))).isTrue(); + + final Long clientId = new FeignClientHelper(fineractClient()).createClient(format(businessDate)); + final Long existingAccountId = ok(() -> fineractClient().fixedDepositAccount() + .submitApplicationFixedDepositAccount(fixedDepositAccountRequest(clientId, productId, businessDate))).getResourceId(); + + final LocalDate startDate = businessDate.minusDays(2); + final LocalDate closeDate = businessDate.minusDays(1); + ok(() -> fineractClient().fixedDepositProduct().updateFixedDepositProduct(productId, new PutFixedDepositProductsProductIdRequest() + .startDate(format(startDate)).closeDate(format(closeDate)).dateFormat(Utils.DATE_FORMAT).locale(Utils.LOCALE))); + + final GetFixedDepositProductsProductIdResponse product = ok( + () -> fineractClient().fixedDepositProduct().retrieveOneFixedDepositProduct(productId)); + assertThat(product.getStartDate()).isEqualTo(startDate); + assertThat(product.getCloseDate()).isEqualTo(closeDate); + assertThat(product.getStatus()).isEqualTo(INACTIVE_STATUS); + + final GetFixedDepositAccountsTemplateResponse archivedTemplate = ok( + () -> fineractClient().fixedDepositAccount().retrieveTemplateFixedDepositAccount(null, null, null, false)); + assertThat(archivedTemplate.getProductOptions().stream().anyMatch(option -> productId.equals(option.getId()))).isFalse(); + + final GetFixedDepositAccountsAccountIdResponse existingAccount = ok( + () -> fineractClient().fixedDepositAccount().retrieveOneFixedDepositAccount(existingAccountId, false, "all")); + assertThat(existingAccount.getId()).isEqualTo(existingAccountId); + + assertApplicationDateError( + fail(() -> fineractClient().fixedDepositAccount() + .submitApplicationFixedDepositAccount(fixedDepositAccountRequest(clientId, productId, startDate.minusDays(1)))), + BEFORE_START_DATE_ERROR); + assertApplicationDateError( + fail(() -> fineractClient().fixedDepositAccount() + .submitApplicationFixedDepositAccount(fixedDepositAccountRequest(clientId, productId, businessDate))), + AFTER_CLOSE_DATE_ERROR); + } + + @Test + public void testRecurringDepositProductCanBeArchived() throws JsonProcessingException { + final LocalDate businessDate = businessDate(); + final Long productId = createRecurringDepositProduct(businessDate.minusMonths(3)); + + final GetRecurringDepositAccountsTemplateResponse activeTemplate = ok( + () -> fineractClient().recurringDepositAccount().retrieveTemplateRecurringDepositAccount(null, null, null, false)); + assertThat(activeTemplate.getProductOptions().stream().anyMatch(product -> productId.equals(product.getId()))).isTrue(); + + final Long clientId = new FeignClientHelper(fineractClient()).createClient(format(businessDate)); + final Long existingAccountId = ok(() -> fineractClient().recurringDepositAccount() + .submitApplicationRecurringDepositAccount(recurringDepositAccountRequest(clientId, productId, businessDate))) + .getResourceId(); + + final LocalDate startDate = businessDate.minusDays(2); + final LocalDate closeDate = businessDate.minusDays(1); + ok(() -> fineractClient().recurringDepositProduct().updateRecurringDepositProduct(productId, + new PutRecurringDepositProductsRequest().startDate(format(startDate)).closeDate(format(closeDate)) + .dateFormat(Utils.DATE_FORMAT).locale(Utils.LOCALE))); + + final GetRecurringDepositProductsProductIdResponse product = ok( + () -> fineractClient().recurringDepositProduct().retrieveOneRecurringDepositProduct(productId)); + assertThat(product.getStartDate()).isEqualTo(startDate); + assertThat(product.getCloseDate()).isEqualTo(closeDate); + assertThat(product.getStatus()).isEqualTo(INACTIVE_STATUS); + + final GetRecurringDepositAccountsTemplateResponse archivedTemplate = ok( + () -> fineractClient().recurringDepositAccount().retrieveTemplateRecurringDepositAccount(null, null, null, false)); + assertThat(archivedTemplate.getProductOptions().stream().anyMatch(option -> productId.equals(option.getId()))).isFalse(); + + final GetRecurringDepositAccountsAccountIdResponse existingAccount = ok( + () -> fineractClient().recurringDepositAccount().retrieveOneRecurringDepositAccount(existingAccountId, false, "all")); + assertThat(existingAccount.getId()).isEqualTo(existingAccountId); + + assertApplicationDateError(fail(() -> fineractClient().recurringDepositAccount() + .submitApplicationRecurringDepositAccount(recurringDepositAccountRequest(clientId, productId, startDate.minusDays(1)))), + BEFORE_START_DATE_ERROR); + assertApplicationDateError( + fail(() -> fineractClient().recurringDepositAccount() + .submitApplicationRecurringDepositAccount(recurringDepositAccountRequest(clientId, productId, businessDate))), + AFTER_CLOSE_DATE_ERROR); + } + + @Test + public void testArchivedDepositProductsRejectReinvestment() throws JsonProcessingException { + final LocalDate businessDate = businessDate(); + final LocalDate applicationDate = businessDate.minusMonths(8); + final Long fixedDepositProductId = createFixedDepositProduct(applicationDate.minusMonths(1)); + final Long recurringDepositProductId = createRecurringDepositProduct(applicationDate.minusMonths(1)); + final Long clientId = new FeignClientHelper(fineractClient()).createClient(format(applicationDate)); + + final Long fixedDepositAccountId = ok(() -> fineractClient().fixedDepositAccount().submitApplicationFixedDepositAccount( + fixedDepositAccountRequest(clientId, fixedDepositProductId, applicationDate).depositPeriod(6))).getResourceId(); + ok(() -> fineractClient().fixedDepositAccount().handleCommandsFixedDepositAccount(fixedDepositAccountId, + datedCommandRequest("approvedOnDate", applicationDate), "approve")); + ok(() -> fineractClient().fixedDepositAccount().handleCommandsFixedDepositAccount(fixedDepositAccountId, + datedCommandRequest("activatedOnDate", applicationDate), "activate")); + + final Long recurringDepositAccountId = ok(() -> fineractClient().recurringDepositAccount().submitApplicationRecurringDepositAccount( + recurringDepositAccountRequest(clientId, recurringDepositProductId, applicationDate).depositPeriod(6))).getResourceId(); + ok(() -> fineractClient().recurringDepositAccount().handleCommandsRecurringDepositAccount(recurringDepositAccountId, + datedCommandRequest("approvedOnDate", applicationDate), "approve")); + ok(() -> fineractClient().recurringDepositAccount().handleCommandsRecurringDepositAccount(recurringDepositAccountId, + datedCommandRequest("activatedOnDate", applicationDate), "activate")); + + new FeignSchedulerHelper(fineractClient()).executeAndAwaitJob("Update Deposit Accounts Maturity details"); + + final GetFixedDepositAccountsAccountIdResponse fixedDepositAccount = ok( + () -> fineractClient().fixedDepositAccount().retrieveOneFixedDepositAccount(fixedDepositAccountId, false, "all")); + assertThat(fixedDepositAccount.getStatus().getCode()).isEqualTo(MATURED_STATUS); + final GetRecurringDepositAccountsAccountIdResponse recurringDepositAccount = ok(() -> fineractClient().recurringDepositAccount() + .retrieveOneRecurringDepositAccount(recurringDepositAccountId, false, "all")); + assertThat(recurringDepositAccount.getStatus().getCode()).isEqualTo(MATURED_STATUS); + + final LocalDate closeDate = businessDate.minusDays(1); + ok(() -> fineractClient().fixedDepositProduct().updateFixedDepositProduct(fixedDepositProductId, + new PutFixedDepositProductsProductIdRequest().startDate(format(applicationDate)).closeDate(format(closeDate)) + .dateFormat(Utils.DATE_FORMAT).locale(Utils.LOCALE))); + ok(() -> fineractClient().recurringDepositProduct().updateRecurringDepositProduct(recurringDepositProductId, + new PutRecurringDepositProductsRequest().startDate(format(applicationDate)).closeDate(format(closeDate)) + .dateFormat(Utils.DATE_FORMAT).locale(Utils.LOCALE))); + + assertApplicationDateError(fail(() -> fineractClient().fixedDepositAccount() + .handleCommandsFixedDepositAccount(fixedDepositAccountId, reinvestmentClosureRequest(businessDate), "close")), + AFTER_CLOSE_DATE_ERROR); + assertApplicationDateError(fail(() -> fineractClient().recurringDepositAccount() + .handleCommandsRecurringDepositAccount(recurringDepositAccountId, reinvestmentClosureRequest(businessDate), "close")), + AFTER_CLOSE_DATE_ERROR); + } + + private Long createFixedDepositProduct(final LocalDate chartStartDate) { + final PostFixedDepositProductsChartSlabs chartSlab = new PostFixedDepositProductsChartSlabs().description("All terms").periodType(2) + .fromPeriod(1).annualInterestRate(5.0); + final PostFixedDepositProductsCharts chart = new PostFixedDepositProductsCharts().fromDate(format(chartStartDate)) + .dateFormat(Utils.DATE_FORMAT).locale(Utils.LOCALE).chartSlabs(Set.of(chartSlab)); + final PostFixedDepositProductsRequest request = new PostFixedDepositProductsRequest().accountingRule(1).charts(Set.of(chart)) + .currencyCode("USD").depositAmount(100000L).description("Fixed deposit product for archiving test").digitsAfterDecimal(4) + .inMultiplesOf(100).interestCalculationDaysInYearType(365).interestCalculationType(1).interestCompoundingPeriodType(4) + .interestPostingPeriodType(4).locale(Utils.LOCALE).maxDepositTerm(10).maxDepositTermTypeId(3).minDepositTerm(6) + .minDepositTermTypeId(2).name(Utils.uniqueRandomStringGenerator("FIXED_DEPOSIT_PRODUCT_", 6)) + .preClosurePenalApplicable(true).preClosurePenalInterest(2.0).preClosurePenalInterestOnTypeId(1) + .shortName(Utils.uniqueRandomStringGenerator("", 4)); + + return ok(() -> fineractClient().fixedDepositProduct().createFixedDepositProduct(request)).getResourceId(); + } + + private Long createRecurringDepositProduct(final LocalDate chartStartDate) { + final PostRecurringDepositProductsChartSlabs chartSlab = new PostRecurringDepositProductsChartSlabs().description("All terms") + .periodType(2).fromPeriod(1).annualInterestRate(5.0); + final PostRecurringDepositProductsCharts chart = new PostRecurringDepositProductsCharts().fromDate(format(chartStartDate)) + .dateFormat(Utils.DATE_FORMAT).locale(Utils.LOCALE).chartSlabs(Set.of(chartSlab)); + final PostRecurringDepositProductsRequest request = new PostRecurringDepositProductsRequest().accountingRule(1) + .charts(Set.of(chart)).currencyCode("USD").depositAmount(100000L) + .description("Recurring deposit product for archiving test").digitsAfterDecimal(4).inMultiplesOf(100) + .interestCalculationDaysInYearType(365).interestCalculationType(1).interestCompoundingPeriodType(4) + .interestPostingPeriodType(4).locale(Utils.LOCALE).maxDepositAmount(1000000L).maxDepositTerm(10).maxDepositTermTypeId(3) + .minDepositAmount(100L).minDepositTerm(6).minDepositTermTypeId(2) + .name(Utils.uniqueRandomStringGenerator("RECURRING_DEPOSIT_PRODUCT_", 6)).preClosurePenalApplicable(true) + .preClosurePenalInterest(2.0).preClosurePenalInterestOnTypeId(1).shortName(Utils.uniqueRandomStringGenerator("", 4)); + + return ok(() -> fineractClient().recurringDepositProduct().createRecurringDepositProduct(request)).getResourceId(); + } + + private PostFixedDepositAccountsRequest fixedDepositAccountRequest(final Long clientId, final Long productId, + final LocalDate submittedOnDate) { + return new PostFixedDepositAccountsRequest().clientId(clientId).productId(productId).locale(Utils.LOCALE) + .dateFormat(Utils.DATE_FORMAT).submittedOnDate(format(submittedOnDate)).depositAmount(100000F).depositPeriod(14) + .depositPeriodFrequencyId(2L); + } + + private PostRecurringDepositAccountsRequest recurringDepositAccountRequest(final Long clientId, final Long productId, + final LocalDate submittedOnDate) { + return new PostRecurringDepositAccountsRequest().clientId(clientId).productId(productId).locale(Utils.LOCALE) + .dateFormat(Utils.DATE_FORMAT).submittedOnDate(format(submittedOnDate)).depositAmount(2000F).depositPeriod(14) + .depositPeriodFrequencyId(2).isCalendarInherited(false).recurringFrequency(1).recurringFrequencyType(2) + .mandatoryRecommendedDepositAmount(2000L); + } + + private Map datedCommandRequest(final String dateParameter, final LocalDate date) { + return Map.of("locale", Utils.LOCALE, "dateFormat", Utils.DATE_FORMAT, dateParameter, format(date)); + } + + private Map reinvestmentClosureRequest(final LocalDate closedOnDate) { + return Map.of("locale", Utils.LOCALE, "dateFormat", Utils.DATE_FORMAT, "closedOnDate", format(closedOnDate), "onAccountClosureId", + 300); + } + + private void assertApplicationDateError(final CallFailedRuntimeException error, final String expectedErrorCode) + throws JsonProcessingException { + assertThat(error.getStatus()).isEqualTo(403); + assertThat(error.getCause()).isInstanceOf(FeignException.class); + + final FeignException cause = (FeignException) error.getCause(); + final JsonNode errors = ObjectMapperFactory.getShared().readTree(cause.responseBodyAsString()).path("errors"); + assertThat(errors.get(0).path("userMessageGlobalisationCode").asText()).isEqualTo(expectedErrorCode); + } + + private String format(final LocalDate date) { + return Utils.dateFormatter.format(date); + } + + private LocalDate businessDate() { + return new FeignBusinessDateHelper(fineractClient()).getBusinessDate("BUSINESS_DATE").getDate(); + } +}