diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 9c020a68f50..a0d1431bf06 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -2400,6 +2400,15 @@ public CommandWrapperBuilder fixedDepositAccountActivation(final Long accountId) return this; } + public CommandWrapperBuilder fixedDepositAccountUndoActivation(final Long accountId) { + this.actionName = "UNDO_ACTIVATE"; + this.entityName = "FIXEDDEPOSITACCOUNT"; + this.savingsId = accountId; + this.entityId = accountId; + this.href = "/fixeddepositaccounts/" + accountId + "?command=activate"; + return this; + } + public CommandWrapperBuilder closeFixedDepositAccount(final Long accountId) { this.actionName = "CLOSE"; this.entityName = "FIXEDDEPOSITACCOUNT"; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/interestratechart/service/InterestRateChartReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/interestratechart/service/InterestRateChartReadPlatformServiceImpl.java index b7fc82a78ba..66951630f89 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/interestratechart/service/InterestRateChartReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/interestratechart/service/InterestRateChartReadPlatformServiceImpl.java @@ -48,7 +48,10 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; + @Service +@Slf4j public class InterestRateChartReadPlatformServiceImpl implements InterestRateChartReadPlatformService { private final PlatformSecurityContext context; @@ -100,6 +103,8 @@ public Collection retrieveAllWithSlabs(Long productId) { sql.append("WHEN NOT irc.is_primary_grouping_by_amount then ircd.amount_range_to "); sql.append("END"); + log.info("SQL: {}", sql.toString()); + return this.jdbcTemplate.query( con -> con.prepareStatement(sql.toString(), ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE), ps -> ps.setLong(1, productId), this.chartExtractor); @@ -208,7 +213,7 @@ private InterestRateChartExtractor(DatabaseSpecificSQLGenerator sqlGenerator) { .append("from ") .append("m_interest_rate_chart irc left join m_interest_rate_slab ircd on irc.id=ircd.interest_rate_chart_id ") .append(" left join m_interest_incentives iri on iri.interest_rate_slab_id = ircd.id ") - .append(" left join m_code_value code on " + sqlGenerator.castChar("code.id") + " = iri.attribute_value ") + .append(" left join m_code_value code on code.id = iri.attribute_value ") .append("left join m_currency curr on ircd.currency_code= curr.code ") .append("left join m_deposit_product_interest_rate_chart dpirc on irc.id=dpirc.interest_rate_chart_id ") .append("left join m_savings_product sp on sp.id=dpirc.deposit_product_id "); 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 e202dd54ec9..8872788b628 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 @@ -66,6 +66,7 @@ import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.core.service.CommandParameterUtil; import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.account.data.PortfolioAccountData; @@ -191,7 +192,7 @@ public String retrieveOne(@PathParam("accountId") @Parameter(description = "acco this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); - if (!(is(chargeStatus, "all") || is(chargeStatus, "active") || is(chargeStatus, "inactive"))) { + if (!(CommandParameterUtil.is(chargeStatus, "all") || CommandParameterUtil.is(chargeStatus, "active") || CommandParameterUtil.is(chargeStatus, "inactive"))) { throw new UnrecognizedQueryParamException("status", chargeStatus, new Object[] { "all", "active", "inactive" }); } @@ -335,35 +336,28 @@ public String handleCommands(@PathParam("accountId") @Parameter(description = "a final CommandWrapperBuilder builder = new CommandWrapperBuilder().withJson(jsonApiRequest); - CommandProcessingResult result = null; - if (is(commandParam, "reject")) { - final CommandWrapper commandRequest = builder.rejectFixedDepositAccountApplication(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "withdrawnByApplicant")) { - final CommandWrapper commandRequest = builder.withdrawFixedDepositAccountApplication(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "approve")) { - final CommandWrapper commandRequest = builder.approveFixedDepositAccountApplication(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "undoapproval")) { - final CommandWrapper commandRequest = builder.undoFixedDepositAccountApplication(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "activate")) { - final CommandWrapper commandRequest = builder.fixedDepositAccountActivation(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "calculateInterest")) { - final CommandWrapper commandRequest = builder.withNoJsonBody().fixedDepositAccountInterestCalculation(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "postInterest")) { - final CommandWrapper commandRequest = builder.fixedDepositAccountInterestPosting(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "close")) { - final CommandWrapper commandRequest = builder.closeFixedDepositAccount(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "prematureClose")) { - final CommandWrapper commandRequest = builder.prematureCloseFixedDepositAccount(accountId).build(); - result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); - } else if (is(commandParam, "calculatePrematureAmount")) { + CommandWrapper commandRequest = null; + if (CommandParameterUtil.is(commandParam, "reject")) { + commandRequest = builder.rejectFixedDepositAccountApplication(accountId).build(); + } else if (CommandParameterUtil.is(commandParam, "withdrawnByApplicant")) { + commandRequest = builder.withdrawFixedDepositAccountApplication(accountId).build(); + } else if (CommandParameterUtil.is(commandParam, "approve")) { + commandRequest = builder.approveFixedDepositAccountApplication(accountId).build(); + } else if (CommandParameterUtil.is(commandParam, "undoapproval")) { + commandRequest = builder.undoFixedDepositAccountApplication(accountId).build(); + } else if (CommandParameterUtil.is(commandParam, "activate")) { + commandRequest = builder.fixedDepositAccountActivation(accountId).build(); + } else if (CommandParameterUtil.is(commandParam, "undoactivate")) { + commandRequest = builder.fixedDepositAccountUndoActivation(accountId).build(); + } else if (CommandParameterUtil.is(commandParam, "calculateInterest")) { + commandRequest = builder.withNoJsonBody().fixedDepositAccountInterestCalculation(accountId).build(); + } else if (CommandParameterUtil.is(commandParam, "postInterest")) { + commandRequest = builder.fixedDepositAccountInterestPosting(accountId).build(); + } else if (CommandParameterUtil.is(commandParam, "close")) { + commandRequest = builder.closeFixedDepositAccount(accountId).build(); + } else if (CommandParameterUtil.is(commandParam, "prematureClose")) { + commandRequest = builder.prematureCloseFixedDepositAccount(accountId).build(); + } else if (CommandParameterUtil.is(commandParam, "calculatePrematureAmount")) { final JsonElement parsedQuery = this.fromJsonHelper.parse(apiRequestBodyAsJson); final JsonQuery query = JsonQuery.from(apiRequestBodyAsJson, parsedQuery, this.fromJsonHelper); final DepositAccountData account = this.accountPreMatureCalculationPlatformService.calculatePreMatureAmount(accountId, query, @@ -373,19 +367,16 @@ public String handleCommands(@PathParam("accountId") @Parameter(description = "a DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); } - if (result == null) { + if (commandRequest == null) { throw new UnrecognizedQueryParamException("command", commandParam, - new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "calculateInterest", - "postInterest", "close", "prematureClose", "calculatePrematureAmount" }); + new Object[] { "reject", "withdrawnByApplicant", "approve", "undoapproval", "activate", "undoactivate", + "calculateInterest", "postInterest", "close", "prematureClose", "calculatePrematureAmount" }); } - + + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); return this.toApiJsonSerializer.serialize(result); } - private boolean is(final String commandParam, final String commandValue) { - return StringUtils.isNotBlank(commandParam) && commandParam.trim().equalsIgnoreCase(commandValue); - } - @DELETE @Path("{accountId}") @Consumes({ MediaType.APPLICATION_JSON }) @@ -411,7 +402,7 @@ public String accountClosureTemplate(@PathParam("accountId") @Parameter(descript this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); DepositAccountData account = null; - if (is(commandParam, "close")) { + if (CommandParameterUtil.is(commandParam, "close")) { account = this.depositAccountReadPlatformService.retrieveOneWithClosureTemplate(DepositAccountType.FIXED_DEPOSIT, accountId); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java index e46ce1b8206..370837d9342 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/FixedDepositAccount.java @@ -714,6 +714,11 @@ public Map activate(final AppUser currentUser, final JsonCommand return actualChanges; } + @Override + public Map undoActivate(final AppUser currentUser, final JsonCommand command, final LocalDate tenantsTodayDate) { + return super.undoActivate(currentUser, command, tenantsTodayDate); + } + private LocalDate depositStartDate() { // TODO: Support to add deposit start date which can be a date after // account activation date. diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java index b4f90725a09..66ebffd6e90 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java @@ -2630,6 +2630,98 @@ public Map activate(final AppUser currentUser, final JsonCommand return actualChanges; } + protected Map undoActivate(final AppUser currentUser, final JsonCommand command, final LocalDate operationDate) { + + final Map actualChanges = new LinkedHashMap<>(); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource(depositAccountType().resourceName() + SavingsApiConstants.activateAction); + + final SavingsAccountStatusType currentStatus = SavingsAccountStatusType.fromInt(this.status); + if (!SavingsAccountStatusType.ACTIVE.hasStateOf(currentStatus)) { + + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName) + .failWithCodeNoParameterAddedToErrorCode("not.in.active.state"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + this.status = SavingsAccountStatusType.APPROVED.getValue(); + actualChanges.put(SavingsApiConstants.statusParamName, SavingsEnumerations.status(this.status)); + + this.rejectedOnDate = null; + this.rejectedBy = null; + this.withdrawnOnDate = null; + this.withdrawnBy = null; + this.closedOnDate = null; + this.closedBy = null; + this.activatedOnDate = null; + this.activatedBy = currentUser; + this.lockedInUntilDate = calculateDateAccountIsLockedUntil(getActivationLocalDate()); + + if (this.client != null && this.client.isActivatedAfter(operationDate)) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String dateAsString = formatter.format(this.client.getActivationLocalDate()); + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.client.activation.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + if (this.group != null && this.group.isActivatedAfter(operationDate)) { + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String dateAsString = formatter.format(this.client.getActivationLocalDate()); + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.group.activation.date"); + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + final LocalDate approvalDate = getApprovedOnLocalDate(); + if (operationDate.isBefore(approvalDate)) { + + final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(command.extractLocale()); + final String dateAsString = formatter.format(approvalDate); + + baseDataValidator.reset().parameter(SavingsApiConstants.activatedOnDateParamName).value(dateAsString) + .failWithCodeNoParameterAddedToErrorCode("cannot.be.before.approval.date"); + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + validateActivityNotBeforeClientOrGroupTransferDate(SavingsEvent.SAVINGS_UNDO_ACTIVATE, operationDate); + + updateSavingsToApprovedState(); + + return actualChanges; + } + + protected void updateSavingsToApprovedState() { + reverseExistingTransactions(); + + for (SavingsAccountCharge charge : this.charges()) { + charge.resetToOriginal(currency); + } + } + + protected void reverseExistingTransactions() { + Collection retainTransactions = new ArrayList<>(); + for (final SavingsAccountTransaction transaction : this.transactions) { + transaction.reverse(); + if (transaction.getId() != null) { + retainTransactions.add(transaction); + } + } + this.transactions.retainAll(retainTransactions); + } + public void processAccountUponActivation(final boolean isSavingsInterestPostingAtCurrentPeriodEnd, final Integer financialYearBeginningMonth, final AppUser user) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsEvent.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsEvent.java index ca5779939ef..f0f2846661a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsEvent.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsEvent.java @@ -28,6 +28,7 @@ public enum SavingsEvent { SAVINGS_APPLICATION_APPROVED("application.approval"), // SAVINGS_APPLICATION_APPROVAL_UNDO("application.approval.undo"), // SAVINGS_ACTIVATE("activate"), // + SAVINGS_UNDO_ACTIVATE("undo.activate"), // SAVINGS_DEPOSIT("deposit"), // SAVINGS_WITHDRAWAL("withdraw"), // SAVINGS_POST_INTEREST("interest.post"), // diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/UndoActivationFixedDepositAccountCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/UndoActivationFixedDepositAccountCommandHandler.java new file mode 100644 index 00000000000..1c383fdf699 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/UndoActivationFixedDepositAccountCommandHandler.java @@ -0,0 +1,43 @@ +/** + * 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.handler; + +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.savings.service.DepositAccountWritePlatformService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Service +@CommandType(entity = "FIXEDDEPOSITACCOUNT", action = "UNDO_ACTIVATE") +public class UndoActivationFixedDepositAccountCommandHandler implements NewCommandSourceHandler { + + private final DepositAccountWritePlatformService depositAccountWritePlatformService; + + @Transactional + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.depositAccountWritePlatformService.undoActivateFDAccount(command.entityId(), command); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountInterestRateChartReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountInterestRateChartReadPlatformServiceImpl.java index d83b08ecdad..103f50d9b75 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountInterestRateChartReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountInterestRateChartReadPlatformServiceImpl.java @@ -212,7 +212,7 @@ private DepositAccountInterestRateChartExtractor(DatabaseSpecificSQLGenerator sq .append("from ") .append("m_savings_account_interest_rate_chart irc left join m_savings_account_interest_rate_slab ircd on irc.id=ircd.savings_account_interest_rate_chart_id ") .append(" left join m_savings_interest_incentives iri on iri.deposit_account_interest_rate_slab_id =ircd.id ") - .append(" left join m_code_value code on " + sqlGenerator.castChar("code.id") + " = iri.attribute_value ") + .append(" left join m_code_value code on code.id = iri.attribute_value ") .append("left join m_currency curr on ircd.currency_code= curr.code ") .append("left join m_savings_account sa on irc.savings_account_id=sa.id "); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformService.java index 90bb3086172..09f4091735e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformService.java @@ -31,6 +31,8 @@ public interface DepositAccountWritePlatformService { CommandProcessingResult activateFDAccount(Long savingsId, JsonCommand command); + CommandProcessingResult undoActivateFDAccount(Long savingsId, JsonCommand command); + CommandProcessingResult activateRDAccount(Long savingsId, JsonCommand command); CommandProcessingResult updateDepositAmountForRDAccount(Long savingsId, JsonCommand command); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java index d0eaec6688e..867fea64ffb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java @@ -229,6 +229,33 @@ public CommandProcessingResult activateFDAccount(final Long savingsId, final Jso .build(); } + @Transactional + @Override + public CommandProcessingResult undoActivateFDAccount(Long savingsId, JsonCommand command) { + final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId, + DepositAccountType.FIXED_DEPOSIT); + checkClientOrGroupActive(account); + + final Set existingTransactionIds = new HashSet<>(); + final Set existingReversedTransactionIds = new HashSet<>(); + updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); + + final AppUser user = this.context.authenticatedUser(); + + final Map changes = account.undoActivate(user, command, DateUtils.getBusinessLocalDate()); + + postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds); + + return new CommandProcessingResultBuilder() // + .withEntityId(savingsId) // + .withOfficeId(account.officeId()) // + .withClientId(account.clientId()) // + .withGroupId(account.groupId()) // + .withSavingsId(savingsId) // + .with(changes) // + .build(); + } + private Money getActivationCharge(final FixedDepositAccount account) { Money activationChargeAmount = Money.zero(account.getCurrency()); for (SavingsAccountCharge savingsAccountCharge : account.charges()) { diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0127_savings_account_undo_activation.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0127_savings_account_undo_activation.xml new file mode 100644 index 00000000000..c08571d8bc0 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0127_savings_account_undo_activation.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +