This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 8c187f9d17 FINERACT-2471: Implement 'Force Debit' functionality for
Savings Accounts with Configurable Limits
8c187f9d17 is described below
commit 8c187f9d17fb839f26fc888f34a64f7c1f57a802
Author: saifulhuq01 <[email protected]>
AuthorDate: Sun Feb 22 09:56:14 2026 +0530
FINERACT-2471: Implement 'Force Debit' functionality for Savings Accounts
with Configurable Limits
---
.../commands/service/CommandWrapperBuilder.java | 9 ++
.../api/GlobalConfigurationConstants.java | 2 +
.../domain/ConfigurationDomainService.java | 4 +
.../test/stepdef/loan/LoanReAgingStepDef.java | 2 +-
.../global/LoanProductGlobalInitializerStep.java | 2 +-
.../domain/ConfigurationDomainServiceJpa.java | 10 ++
...SavingsAccountForceWithdrawalBusinessEvent.java | 35 +++++++
.../interoperation/service/InteropServiceImpl.java | 4 +-
.../starter/InteroperationConfiguration.java | 7 +-
.../api/SavingsAccountTransactionsApiResource.java | 3 +-
.../savings/domain/DepositAccountAssembler.java | 12 ++-
.../savings/domain/SavingsAccountAssembler.java | 10 +-
.../domain/SavingsAccountDomainServiceJpa.java | 9 +-
...orceWithdrawalSavingsAccountCommandHandler.java | 46 +++++++++
...countWritePlatformServiceJpaRepositoryImpl.java | 63 ++++++++++++
.../db/changelog/tenant/changelog-tenant.xml | 1 +
.../tenant/parts/0217_force_withdrawal_configs.xml | 74 ++++++++++++++
...nalEventConfigurationValidationServiceTest.java | 4 +-
.../savings/SavingsTransactionBooleanValues.java | 18 ++++
.../portfolio/savings/domain/SavingsAccount.java | 51 ++++++++--
.../SavingsAccountWritePlatformService.java | 5 +-
.../GroupSavingsIntegrationTest.java | 1 +
.../SavingsAccountForceWithdrawalTest.java | 113 +++++++++++++++++++++
.../common/ExternalEventConfigurationHelper.java | 20 +++-
.../common/GlobalConfigurationHelper.java | 17 +++-
.../common/savings/SavingsAccountHelper.java | 14 +++
26 files changed, 500 insertions(+), 36 deletions(-)
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 6d778186c8..8493553fb5 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
@@ -3901,4 +3901,13 @@ public class CommandWrapperBuilder {
this.href = "/loans/" + loanId + "/originators/" + originatorId;
return this;
}
+
+ public CommandWrapperBuilder savingsAccountForceWithdrawal(final Long
accountId) {
+ this.actionName = "FORCE_WITHDRAWAL";
+ this.entityName = "SAVINGSACCOUNT";
+ this.entityId = accountId;
+ this.savingsId = accountId;
+ this.href = "/savingsaccounts/" + accountId;
+ return this;
+ }
}
diff --git
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java
index a9336214d1..edaa38bc97 100644
---
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java
+++
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java
@@ -81,6 +81,8 @@ public final class GlobalConfigurationConstants {
public static final String
ALLOWED_LOAN_STATUSES_OF_DELAYED_SETTLEMENT_FOR_EXTERNAL_ASSET_TRANSFER =
"allowed-loan-statuses-of-delayed-settlement-for-external-asset-transfer";
public static final String
ENABLE_ORIGINATOR_CREATION_DURING_LOAN_APPLICATION =
"enable-originator-creation-during-loan-application";
public static final String PASSWORD_REUSE_CHECK_HISTORY_COUNT =
"password-reuse-check-history-count";
+ public static final String FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT =
"allow-force-withdrawal-on-savings-account";
+ public static final String FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT_LIMIT =
"force-withdrawal-on-savings-account-limit";
public static final String FORCE_PASSWORD_RESET_ON_FIRST_LOGIN =
"force-password-reset-on-first-login";
private GlobalConfigurationConstants() {}
diff --git
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
index b2a897b465..2f88120cf2 100644
---
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
+++
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java
@@ -152,6 +152,10 @@ public interface ConfigurationDomainService {
String getAssetOwnerTransferOustandingInterestStrategy();
+ boolean isForceWithdrawalOnSavingsAccountEnabled();
+
+ Long retrieveForceWithdrawalOnSavingsAccountLimit();
+
Integer getPasswordReuseRestrictionCount();
boolean isForcePasswordResetOnFirstLoginEnabled();
diff --git
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java
index d70acbe9a3..eb5f1fc983 100644
---
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java
+++
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanReAgingStepDef.java
@@ -366,7 +366,7 @@ public class LoanReAgingStepDef extends AbstractStepDef {
PostLoansLoanIdTransactionsRequest
setReAgeingRequestProperties(PostLoansLoanIdTransactionsRequest request,
List<String> headers,
List<String> values) {
for (int i = 0; i < headers.size(); i++) {
- String header = headers.get(i).toLowerCase().trim().replaceAll("
", "");
+ String header =
headers.get(i).toLowerCase(java.util.Locale.ROOT).trim().replaceAll(" ", "");
switch (header) {
case "frequencynumber" ->
request.setFrequencyNumber(Integer.parseInt(values.get(i)));
case "frequencytype" ->
request.setFrequencyType(values.get(i));
diff --git
a/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
index 687762fb7b..b44a29d6a4 100644
---
a/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
+++
b/fineract-e2e-tests-runner/src/test/java/org/apache/fineract/test/initializer/global/LoanProductGlobalInitializerStep.java
@@ -1312,7 +1312,7 @@ public class LoanProductGlobalInitializerStep implements
FineractGlobalInitializ
.recalculationRestFrequencyType(1)//
.recalculationRestFrequencyInterval(1)//
.repaymentEvery(1)//
- .interestRatePerPeriod((double) 7.0)//
+ .interestRatePerPeriod(7.0)//
.interestRateFrequencyType(INTEREST_RATE_FREQUENCY_TYPE_MONTH)//
.enableDownPayment(false)//
.interestRecalculationCompoundingMethod(0)//
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java
index e43ee3396f..42838c3064 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java
@@ -549,6 +549,16 @@ public class ConfigurationDomainServiceJpa implements
ConfigurationDomainService
GlobalConfigurationConstants.ASSET_OWNER_TRANSFER_OUTSTANDING_INTEREST_CALCULATION_STRATEGY).getStringValue();
}
+ @Override
+ public boolean isForceWithdrawalOnSavingsAccountEnabled() {
+ return
getGlobalConfigurationPropertyData(GlobalConfigurationConstants.FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT).isEnabled();
+ }
+
+ @Override
+ public Long retrieveForceWithdrawalOnSavingsAccountLimit() {
+ return
getGlobalConfigurationPropertyData(GlobalConfigurationConstants.FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT_LIMIT).getValue();
+ }
+
@Override
public Integer getPasswordReuseRestrictionCount() {
final GlobalConfigurationPropertyData property =
getGlobalConfigurationPropertyData(
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/savings/transaction/SavingsAccountForceWithdrawalBusinessEvent.java
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/savings/transaction/SavingsAccountForceWithdrawalBusinessEvent.java
new file mode 100644
index 0000000000..f7d2e5c9c9
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/business/domain/savings/transaction/SavingsAccountForceWithdrawalBusinessEvent.java
@@ -0,0 +1,35 @@
+/**
+ * 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.infrastructure.event.business.domain.savings.transaction;
+
+import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
+
+public class SavingsAccountForceWithdrawalBusinessEvent extends
SavingsAccountTransactionBusinessEvent {
+
+ private static final String TYPE =
"SavingsAccountForceWithdrawalBusinessEvent";
+
+ public
SavingsAccountForceWithdrawalBusinessEvent(SavingsAccountTransaction value) {
+ super(value);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java
index 23157ed7e1..a8414585e6 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java
@@ -44,6 +44,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.service.CommandWrapperBuilder;
import
org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
+import
org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
@@ -134,6 +135,7 @@ public class InteropServiceImpl implements InteropService {
private final SavingsAccountTransactionSummaryWrapper
savingsAccountTransactionSummaryWrapper;
private final SavingsAccountDomainService savingsAccountService;
+ private final ConfigurationDomainService configurationDomainService;
private final JdbcTemplate jdbcTemplate;
@@ -566,7 +568,7 @@ public class InteropServiceImpl implements InteropService {
private SavingsAccount validateAndGetSavingAccount(@NonNull
InteropRequestData request) {
// TODO: error handling
SavingsAccount savingsAccount =
validateAndGetSavingAccount(request.getAccountId());
- savingsAccount.setHelpers(savingsAccountTransactionSummaryWrapper,
savingsHelper);
+ savingsAccount.setHelpers(savingsAccountTransactionSummaryWrapper,
savingsHelper, configurationDomainService);
ApplicationCurrency requestCurrency =
currencyRepository.findOneByCode(request.getAmount().getCurrency());
if
(!savingsAccount.getCurrency().getCode().equals(requestCurrency.getCode())) {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/interoperation/starter/InteroperationConfiguration.java
b/fineract-provider/src/main/java/org/apache/fineract/interoperation/starter/InteroperationConfiguration.java
index 325baf5884..69ca7dfefd 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/interoperation/starter/InteroperationConfiguration.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/interoperation/starter/InteroperationConfiguration.java
@@ -19,6 +19,7 @@
package org.apache.fineract.interoperation.starter;
import
org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
+import
org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import
org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import
org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import
org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
@@ -52,12 +53,12 @@ public class InteroperationConfiguration {
PaymentTypeRepository paymentTypeRepository,
InteropIdentifierRepository identifierRepository,
LoanRepositoryWrapper loanRepositoryWrapper, SavingsHelper
savingsHelper,
SavingsAccountTransactionSummaryWrapper
savingsAccountTransactionSummaryWrapper,
- SavingsAccountDomainService savingsAccountService, JdbcTemplate
jdbcTemplate,
- PortfolioCommandSourceWritePlatformService
commandsSourceWritePlatformService,
+ SavingsAccountDomainService savingsAccountService,
ConfigurationDomainService configurationDomainService,
+ JdbcTemplate jdbcTemplate,
PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService,
DefaultToApiJsonSerializer<LoanAccountData> toApiJsonSerializer,
DatabaseSpecificSQLGenerator sqlGenerator) {
return new InteropServiceImpl(securityContext, interopDataValidator,
savingsAccountRepository, savingsAccountTransactionRepository,
applicationCurrencyRepository, noteRepository,
paymentTypeRepository, identifierRepository, loanRepositoryWrapper,
- savingsHelper, savingsAccountTransactionSummaryWrapper,
savingsAccountService, jdbcTemplate,
+ savingsHelper, savingsAccountTransactionSummaryWrapper,
savingsAccountService, configurationDomainService, jdbcTemplate,
commandsSourceWritePlatformService, toApiJsonSerializer,
sqlGenerator);
}
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java
index 68ee87d8d5..93825b0e5d 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java
@@ -179,9 +179,10 @@ public class SavingsAccountTransactionsApiResource {
case "deposit" -> builder.savingsAccountDeposit(savingsId).build();
case "gsimDeposit" ->
builder.gsimSavingsAccountDeposit(savingsId).build();
case "withdrawal" ->
builder.savingsAccountWithdrawal(savingsId).build();
+ case "force-withdrawal" ->
builder.savingsAccountForceWithdrawal(savingsId).build();
case "postInterestAsOn" ->
builder.savingsAccountInterestPosting(savingsId).build();
case SavingsApiConstants.COMMAND_HOLD_AMOUNT ->
builder.holdAmount(savingsId).build();
- default -> throw new UnrecognizedQueryParamException("command",
commandParam, "deposit", "withdrawal",
+ default -> throw new UnrecognizedQueryParamException("command",
commandParam, "deposit", "withdrawal", "force-withdrawal",
SavingsApiConstants.COMMAND_HOLD_AMOUNT);
};
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 3342eeec4d..e1ea9fd2b5 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
@@ -65,6 +65,7 @@ import java.util.Collection;
import java.util.Locale;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
+import
org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import
org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException;
@@ -120,6 +121,7 @@ public class DepositAccountAssembler {
private final PaymentDetailAssembler paymentDetailAssembler;
private final ExternalIdFactory externalIdFactory;
+ private final ConfigurationDomainService configurationDomainService;
@Autowired
public DepositAccountAssembler(final
SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper,
@@ -130,7 +132,8 @@ public class DepositAccountAssembler {
final DepositProductAssembler depositProductAssembler,
final RecurringDepositProductRepository
recurringDepositProductRepository,
final AccountTransfersReadPlatformService
accountTransfersReadPlatformService, final PlatformSecurityContext context,
- final PaymentDetailAssembler paymentDetailAssembler,
ExternalIdFactory externalIdFactory) {
+ final PaymentDetailAssembler paymentDetailAssembler,
ExternalIdFactory externalIdFactory,
+ final ConfigurationDomainService configurationDomainService) {
this.savingsAccountTransactionSummaryWrapper =
savingsAccountTransactionSummaryWrapper;
this.clientRepository = clientRepository;
@@ -146,6 +149,7 @@ public class DepositAccountAssembler {
this.context = context;
this.paymentDetailAssembler = paymentDetailAssembler;
this.externalIdFactory = externalIdFactory;
+ this.configurationDomainService = configurationDomainService;
}
/**
@@ -356,7 +360,7 @@ public class DepositAccountAssembler {
}
if (account != null) {
- account.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper);
+ account.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper, this.configurationDomainService);
account.validateNewApplicationState(depositAccountType.resourceName());
}
@@ -365,12 +369,12 @@ public class DepositAccountAssembler {
public SavingsAccount assembleFrom(final Long savingsId,
DepositAccountType depositAccountType) {
final SavingsAccount account =
this.savingsAccountRepository.findOneWithNotFoundDetection(savingsId,
depositAccountType);
- account.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper);
+ account.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper, this.configurationDomainService);
return account;
}
public void assignSavingAccountHelpers(final SavingsAccount
savingsAccount) {
-
savingsAccount.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper);
+
savingsAccount.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper, this.configurationDomainService);
}
public DepositAccountTermAndPreClosure
assembleAccountTermAndPreClosure(final JsonCommand command,
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java
index 0fd16e6dbb..2574ba5fa9 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java
@@ -330,7 +330,7 @@ public class SavingsAccountAssembler {
minRequiredOpeningBalance, lockinPeriodFrequency,
lockinPeriodFrequencyType, iswithdrawalFeeApplicableForTransfer, charges,
allowOverdraft, overdraftLimit, enforceMinRequiredBalance,
minRequiredBalance, maxAllowedLienLimit, lienAllowed,
nominalAnnualInterestRateOverdraft,
minOverdraftForInterestCalculation, withHoldTax);
- account.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper);
+ account.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper, this.configurationDomainService);
account.validateNewApplicationState(SAVINGS_ACCOUNT_RESOURCE_NAME);
@@ -381,7 +381,7 @@ public class SavingsAccountAssembler {
}
}
- account.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper);
+ account.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper, this.configurationDomainService);
return account;
}
@@ -421,7 +421,7 @@ public class SavingsAccountAssembler {
}
public void setHelpers(final SavingsAccount account) {
- account.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper);
+ account.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper, this.configurationDomainService);
}
/**
@@ -465,7 +465,7 @@ public class SavingsAccountAssembler {
product.isMinRequiredBalanceEnforced(),
product.minRequiredBalance(), product.maxAllowedLienLimit(),
product.isLienAllowed(),
product.nominalAnnualInterestRateOverdraft(),
product.minOverdraftForInterestCalculation(),
product.withHoldTax());
- account.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper);
+ account.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper, this.configurationDomainService);
account.validateNewApplicationState(SAVINGS_ACCOUNT_RESOURCE_NAME);
@@ -475,7 +475,7 @@ public class SavingsAccountAssembler {
}
public void assignSavingAccountHelpers(final SavingsAccount
savingsAccount) {
-
savingsAccount.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper);
+
savingsAccount.setHelpers(this.savingsAccountTransactionSummaryWrapper,
this.savingsHelper, this.configurationDomainService);
}
public void assignSavingAccountHelpers(final SavingsAccountData
savingsAccountData) {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java
index fb16621dc6..efc877587e 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java
@@ -31,6 +31,7 @@ import java.util.UUID;
import
org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import
org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
+import
org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsAccountForceWithdrawalBusinessEvent;
import
org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsDepositBusinessEvent;
import
org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsWithdrawalBusinessEvent;
import
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
@@ -130,7 +131,7 @@ public class SavingsAccountDomainServiceJpa implements
SavingsAccountDomainServi
}
account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount,
transactionBooleanValues.isExceptionForBalanceCheck(),
- depositAccountOnHoldTransactions, backdatedTxnsAllowedTill);
+ depositAccountOnHoldTransactions, backdatedTxnsAllowedTill,
transactionBooleanValues.isForceWithdrawal());
saveTransactionToGenerateTransactionId(withdrawal);
if (backdatedTxnsAllowedTill) {
@@ -142,7 +143,11 @@ public class SavingsAccountDomainServiceJpa implements
SavingsAccountDomainServi
postJournalEntries(account, existingTransactionIds,
existingReversedTransactionIds, transactionBooleanValues.isAccountTransfer(),
backdatedTxnsAllowedTill);
- businessEventNotifierService.notifyPostBusinessEvent(new
SavingsWithdrawalBusinessEvent(withdrawal));
+ if (transactionBooleanValues.isForceWithdrawal()) {
+ businessEventNotifierService.notifyPostBusinessEvent(new
SavingsAccountForceWithdrawalBusinessEvent(withdrawal));
+ } else {
+ businessEventNotifierService.notifyPostBusinessEvent(new
SavingsWithdrawalBusinessEvent(withdrawal));
+ }
return withdrawal;
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/ForceWithdrawalSavingsAccountCommandHandler.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/ForceWithdrawalSavingsAccountCommandHandler.java
new file mode 100644
index 0000000000..c0c96a38b8
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/handler/ForceWithdrawalSavingsAccountCommandHandler.java
@@ -0,0 +1,46 @@
+/**
+ * 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.SavingsAccountWritePlatformService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@CommandType(entity = "SAVINGSACCOUNT", action = "FORCE_WITHDRAWAL")
+public class ForceWithdrawalSavingsAccountCommandHandler implements
NewCommandSourceHandler {
+
+ private final SavingsAccountWritePlatformService writePlatformService;
+
+ @Autowired
+ public ForceWithdrawalSavingsAccountCommandHandler(final
SavingsAccountWritePlatformService writePlatformService) {
+ this.writePlatformService = writePlatformService;
+ }
+
+ @Transactional
+ @Override
+ public CommandProcessingResult processCommand(final JsonCommand command) {
+ return
this.writePlatformService.forceWithdrawal(command.getSavingsId(), command);
+ }
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java
index f30712b509..c5335a438c 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformServiceJpaRepositoryImpl.java
@@ -405,6 +405,69 @@ public class
SavingsAccountWritePlatformServiceJpaRepositoryImpl implements Savi
.build();
}
+ @Transactional
+ @Override
+ public CommandProcessingResult forceWithdrawal(final Long savingsId, final
JsonCommand command) {
+
+ this.savingsAccountTransactionDataValidator.validate(command);
+
+ boolean isGsim = false;
+
+ final LocalDate transactionDate =
command.localDateValueOfParameterNamed("transactionDate");
+ final BigDecimal transactionAmount =
command.bigDecimalValueOfParameterNamed("transactionAmount");
+
+ final Locale locale = command.extractLocale();
+ final DateTimeFormatter fmt =
DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
+
+ final Map<String, Object> changes = new LinkedHashMap<>();
+ final PaymentDetail paymentDetail =
this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command,
changes);
+
+ final boolean backdatedTxnsAllowedTill =
this.savingAccountAssembler.getPivotConfigStatus();
+
+ final SavingsAccount account =
this.savingAccountAssembler.assembleFrom(savingsId, backdatedTxnsAllowedTill);
+
+ if (account.getGsim() != null) {
+ isGsim = true;
+ }
+ checkClientOrGroupActive(account);
+
+
this.savingsAccountTransactionDataValidator.validateTransactionWithPivotDate(transactionDate,
account);
+
+ final boolean isAccountTransfer = false;
+ final boolean isRegularTransaction = true;
+ final boolean isApplyWithdrawFee = true;
+ final boolean isInterestTransfer = false;
+ final boolean isWithdrawBalance = false;
+ final boolean isForceWithdrawal = true;
+ final SavingsTransactionBooleanValues transactionBooleanValues = new
SavingsTransactionBooleanValues(isAccountTransfer,
+ isRegularTransaction, isApplyWithdrawFee, isInterestTransfer,
isWithdrawBalance, isForceWithdrawal);
+ final SavingsAccountTransaction withdrawal =
this.savingsAccountDomainService.handleWithdrawal(account, fmt, transactionDate,
+ transactionAmount, paymentDetail, transactionBooleanValues,
backdatedTxnsAllowedTill);
+
+ if (isGsim && (withdrawal.getId() != null)) {
+ GroupSavingsIndividualMonitoring gsim =
gsimRepository.findById(account.getGsim().getId()).orElseThrow();
+ BigDecimal currentBalance =
gsim.getParentDeposit().subtract(transactionAmount);
+ gsim.setParentDeposit(currentBalance);
+ gsimRepository.save(gsim);
+
+ }
+
+ final String noteText = command.stringValueOfParameterNamed("note");
+ if (StringUtils.isNotBlank(noteText)) {
+ final Note note = Note.savingsTransactionNote(account, withdrawal,
noteText);
+ this.noteRepository.save(note);
+ }
+
+ return new CommandProcessingResultBuilder() //
+ .withEntityId(withdrawal.getId()) //
+ .withOfficeId(account.officeId()) //
+ .withClientId(account.clientId()) //
+ .withGroupId(account.groupId()) //
+ .withSavingsId(savingsId) //
+ .with(changes)//
+ .build();
+ }
+
@Transactional
@Override
public CommandProcessingResult applyAnnualFee(final Long
savingsAccountChargeId, final Long accountId) {
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 faf3484d67..38143a7df6 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
@@ -235,4 +235,5 @@
<include file="parts/0214_trial_balance_summary_adding_originators.xml"
relativeToChangelogFile="true" />
<include
file="parts/0215_transaction_summary_reports_add_buydown_fee_types.xml"
relativeToChangelogFile="true" />
<include file="parts/0216_add_unique_constraint_sms_campaign_name.xml"
relativeToChangelogFile="true" />
+ <include file="parts/0217_force_withdrawal_configs.xml"
relativeToChangelogFile="true" />
</databaseChangeLog>
diff --git
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0217_force_withdrawal_configs.xml
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0217_force_withdrawal_configs.xml
new file mode 100644
index 0000000000..575ca65cb8
--- /dev/null
+++
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0217_force_withdrawal_configs.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
+
+ <changeSet author="fineract" id="1">
+ <insert tableName="c_configuration">
+ <column name="name"
value="allow-force-withdrawal-on-savings-account"/>
+ <column name="value" valueNumeric="0"/>
+ <column name="enabled" valueBoolean="false"/>
+ <column name="is_trap_door" valueBoolean="false"/>
+ <column name="description" value="If enabled, allows withdrawals
to put the account into negative balance up to the configured limit."/>
+ </insert>
+
+ <insert tableName="c_configuration">
+ <column name="name"
value="force-withdrawal-on-savings-account-limit"/>
+ <column name="value" valueNumeric="0"/>
+ <column name="enabled" valueBoolean="false"/>
+ <column name="is_trap_door" valueBoolean="false"/>
+ <column name="description" value="The maximum negative balance
allowed when force withdrawal is enabled."/>
+ </insert>
+
+ <insert tableName="m_permission">
+ <column name="grouping" value="portfolio"/>
+ <column name="code" value="FORCE_WITHDRAWAL_SAVINGSACCOUNT"/>
+ <column name="entity_name" value="SAVINGSACCOUNT"/>
+ <column name="action_name" value="FORCE_WITHDRAWAL"/>
+ <column name="can_maker_checker" valueBoolean="true"/>
+ </insert>
+
+ <insert tableName="m_permission">
+ <column name="grouping" value="portfolio"/>
+ <column name="code"
value="FORCE_WITHDRAWAL_SAVINGSACCOUNT_CHECKER"/>
+ <column name="entity_name" value="SAVINGSACCOUNT"/>
+ <column name="action_name" value="FORCE_WITHDRAWAL_CHECKER"/>
+ <column name="can_maker_checker" valueBoolean="false"/>
+ </insert>
+ </changeSet>
+
+ <changeSet author="fineract" id="2">
+ <sql>
+ INSERT INTO m_role_permission (role_id, permission_id)
+ SELECT 1, p.id FROM m_permission p
+ WHERE p.code IN ('FORCE_WITHDRAWAL_SAVINGSACCOUNT',
'FORCE_WITHDRAWAL_SAVINGSACCOUNT_CHECKER')
+ AND NOT EXISTS (SELECT 1 FROM m_role_permission rp WHERE
rp.role_id = 1 AND rp.permission_id = p.id);
+ </sql>
+ </changeSet>
+ <changeSet author="fineract" id="3">
+ <insert tableName="m_external_event_configuration">
+ <column name="type"
value="SavingsAccountForceWithdrawalBusinessEvent"/>
+ <column name="enabled" valueBoolean="false"/>
+ </insert>
+ </changeSet>
+</databaseChangeLog>
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
index b80cd8b4bc..f9d7bf1de6 100644
---
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
+++
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
@@ -113,7 +113,7 @@ public class
ExternalEventConfigurationValidationServiceTest {
"LoanBuyDownFeeTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanApprovedAmountChangedBusinessEvent",
- "SavingsAccountsStayedLockedBusinessEvent");
+ "SavingsAccountsStayedLockedBusinessEvent",
"SavingsAccountForceWithdrawalBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default
Tenant", "Europe/Budapest", null));
@@ -209,7 +209,7 @@ public class
ExternalEventConfigurationValidationServiceTest {
"LoanBuyDownFeeTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAdjustmentTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationTransactionCreatedBusinessEvent",
"LoanBuyDownFeeAmortizationAdjustmentTransactionCreatedBusinessEvent",
"LoanApprovedAmountChangedBusinessEvent",
- "SavingsAccountsStayedLockedBusinessEvent");
+ "SavingsAccountsStayedLockedBusinessEvent",
"SavingsAccountForceWithdrawalBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default
Tenant", "Europe/Budapest", null));
diff --git
a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/SavingsTransactionBooleanValues.java
b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/SavingsTransactionBooleanValues.java
index 2cef4adfc7..de39dfb4d6 100644
---
a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/SavingsTransactionBooleanValues.java
+++
b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/SavingsTransactionBooleanValues.java
@@ -25,6 +25,19 @@ public class SavingsTransactionBooleanValues {
private final boolean isApplyWithdrawFee;
private final boolean isInterestTransfer;
private final boolean isExceptionForBalanceCheck;
+ private final boolean isForceWithdrawal;
+
+ public SavingsTransactionBooleanValues(final boolean isAccountTransfer,
final boolean isRegularTransaction,
+ final boolean isApplyWithdrawFee, final boolean
isInterestTransfer, final boolean isExceptionForBalanceCheck,
+ final boolean isForceWithdrawal) {
+
+ this.isAccountTransfer = isAccountTransfer;
+ this.isRegularTransaction = isRegularTransaction;
+ this.isApplyWithdrawFee = isApplyWithdrawFee;
+ this.isInterestTransfer = isInterestTransfer;
+ this.isExceptionForBalanceCheck = isExceptionForBalanceCheck;
+ this.isForceWithdrawal = isForceWithdrawal;
+ }
public SavingsTransactionBooleanValues(final boolean isAccountTransfer,
final boolean isRegularTransaction,
final boolean isApplyWithdrawFee, final boolean
isInterestTransfer, final boolean isExceptionForBalanceCheck) {
@@ -34,6 +47,7 @@ public class SavingsTransactionBooleanValues {
this.isApplyWithdrawFee = isApplyWithdrawFee;
this.isInterestTransfer = isInterestTransfer;
this.isExceptionForBalanceCheck = isExceptionForBalanceCheck;
+ this.isForceWithdrawal = false;
}
public boolean isAccountTransfer() {
@@ -56,4 +70,8 @@ public class SavingsTransactionBooleanValues {
return this.isExceptionForBalanceCheck;
}
+ public boolean isForceWithdrawal() {
+ return this.isForceWithdrawal;
+ }
+
}
diff --git
a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
index 7bad316ec9..c537c22cfc 100644
---
a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
+++
b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccount.java
@@ -459,9 +459,10 @@ public class SavingsAccount extends
AbstractAuditableWithUTCDateTimeCustom<Long>
* update summary details after events/transactions on a {@link
SavingsAccount}.
*/
public void setHelpers(final SavingsAccountTransactionSummaryWrapper
savingsAccountTransactionSummaryWrapper,
- final SavingsHelper savingsHelper) {
+ final SavingsHelper savingsHelper, final
ConfigurationDomainService configurationDomainService) {
this.savingsAccountTransactionSummaryWrapper =
savingsAccountTransactionSummaryWrapper;
this.savingsHelper = savingsHelper;
+ this.configurationDomainService = configurationDomainService;
}
public void setSavingsAccountTransactions(final
List<SavingsAccountTransaction> savingsAccountTransactions) {
@@ -822,7 +823,8 @@ public class SavingsAccount extends
AbstractAuditableWithUTCDateTimeCustom<Long>
boolean isInterestTransfer, final boolean
isSavingsInterestPostingAtCurrentPeriodEnd, final Integer
financialYearBeginningMonth,
final LocalDate postInterestOnDate, final boolean
backdatedTxnsAllowedTill, final boolean postReversals) {
- // no openingBalance concept supported yet but probably will to allow
for migrations.
+ // no openingBalance concept supported yet but probably will to allow
for
+ // migrations.
// Check global configurations and 'pivot' date is null
Money openingAccountBalance = backdatedTxnsAllowedTill ?
Money.of(this.currency, this.summary.getRunningBalanceOnPivotDate())
: Money.zero(this.currency);
@@ -833,7 +835,8 @@ public class SavingsAccount extends
AbstractAuditableWithUTCDateTimeCustom<Long>
final List<PostingPeriod> allPostingPeriods = new ArrayList<>();
if (hasInterestCalculation() || hasOverdraftInterestCalculation()) {
// 1. default to calculate interest based on entire history OR
- // 2. determine latest 'posting period' and find interest credited
to that period
+ // 2. determine latest 'posting period' and find interest credited
to that
+ // period
// A generate list of EndOfDayBalances (not including interest
postings)
final SavingsPostingInterestPeriodType postingPeriodType =
SavingsPostingInterestPeriodType
@@ -1460,7 +1463,8 @@ public class SavingsAccount extends
AbstractAuditableWithUTCDateTimeCustom<Long>
}
public void validateAccountBalanceDoesNotBecomeNegative(final BigDecimal
transactionAmount, final boolean isException,
- final List<DepositAccountOnHoldTransaction>
depositAccountOnHoldTransactions, final boolean backdatedTxnsAllowedTill) {
+ final List<DepositAccountOnHoldTransaction>
depositAccountOnHoldTransactions, final boolean backdatedTxnsAllowedTill,
+ final boolean isForceWithdrawal) {
List<SavingsAccountTransaction> transactionsSortedByDate = null;
@@ -1508,7 +1512,8 @@ public class SavingsAccount extends
AbstractAuditableWithUTCDateTimeCustom<Long>
// deal with potential minRequiredBalance and
// enforceMinRequiredBalance
if (!isException && transaction.canProcessBalanceCheck() &&
!isOverdraft()) {
- if (runningBalance.minus(minRequiredBalance).isLessThanZero())
{
+ if (runningBalance.minus(minRequiredBalance).isLessThanZero()
+ && !isForceWithdrawalAllowed(isForceWithdrawal,
runningBalance)) {
throw new
InsufficientAccountBalanceException("transactionAmount", getAccountBalance(),
withdrawalFee,
transactionAmount);
}
@@ -1521,7 +1526,7 @@ public class SavingsAccount extends
AbstractAuditableWithUTCDateTimeCustom<Long>
// interest posting
// and should be checked after processing all transactions
if (isOverdraft()) {
- if (runningBalance.minus(minRequiredBalance).isLessThanZero()) {
+ if (runningBalance.minus(minRequiredBalance).isLessThanZero() &&
!isForceWithdrawalAllowed(isForceWithdrawal, runningBalance)) {
throw new
InsufficientAccountBalanceException("transactionAmount", getAccountBalance(),
withdrawalFee, transactionAmount);
}
}
@@ -1541,6 +1546,31 @@ public class SavingsAccount extends
AbstractAuditableWithUTCDateTimeCustom<Long>
}
}
+ /**
+ * Checks whether a force withdrawal is allowed based on the global
configuration and the configured negative
+ * balance limit.
+ *
+ * @param isForceWithdrawal
+ * whether the current transaction is a force withdrawal
+ * @param runningBalance
+ * the current running balance of the account
+ * @return true if force withdrawal is enabled and the running balance is
within the allowed negative limit
+ */
+ private boolean isForceWithdrawalAllowed(final boolean isForceWithdrawal,
final Money runningBalance) {
+ if (!isForceWithdrawal || this.configurationDomainService == null) {
+ return false;
+ }
+ if
(!this.configurationDomainService.isForceWithdrawalOnSavingsAccountEnabled()) {
+ return false;
+ }
+ Long limit =
this.configurationDomainService.retrieveForceWithdrawalOnSavingsAccountLimit();
+ BigDecimal limitBd = BigDecimal.valueOf(limit);
+ if (limitBd.compareTo(BigDecimal.ZERO) > 0) {
+ limitBd = limitBd.negate();
+ }
+ return runningBalance.getAmount().compareTo(limitBd) >= 0;
+ }
+
public void validateAccountBalanceDoesNotBecomeNegative(final String
transactionAction,
final List<DepositAccountOnHoldTransaction>
depositAccountOnHoldTransactions, final boolean backdatedTxnsAllowedTill) {
@@ -2470,14 +2500,16 @@ public class SavingsAccount extends
AbstractAuditableWithUTCDateTimeCustom<Long>
}
public void validateAccountBalanceDoesNotBecomeNegativeMinimal(final
BigDecimal transactionAmount, final boolean isException) {
- // final List<SavingsAccountTransaction> transactionsSortedByDate =
retrieveListOfTransactions();
+ // final List<SavingsAccountTransaction> transactionsSortedByDate =
+ // retrieveListOfTransactions();
Money runningBalance = this.summary.getAccountBalance(getCurrency());
Money minRequiredBalance = minRequiredBalanceDerived(getCurrency());
final BigDecimal withdrawalFee = null;
// check last txn date
- // In overdraft cases, minRequiredBalance can be in violation after
interest posting
+ // In overdraft cases, minRequiredBalance can be in violation after
interest
+ // posting
// and should be checked after processing all transactions
if (!isOverdraft()) {
if (runningBalance.minus(minRequiredBalance).isLessThanZero()) {
@@ -2708,7 +2740,8 @@ public class SavingsAccount extends
AbstractAuditableWithUTCDateTimeCustom<Long>
charge.updateToNextDueDateFrom(getActivationDate());
}
- // auto pay the activation time charges (No need of checking the pivot
date config)
+ // auto pay the activation time charges (No need of checking the pivot
date
+ // config)
this.payActivationCharges(isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, false);
// TODO : AA add activation charges to actual changes list
}
diff --git
a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java
b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java
index 8b29795919..965aee5eb7 100644
---
a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java
+++
b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountWritePlatformService.java
@@ -91,7 +91,8 @@ public interface SavingsAccountWritePlatformService {
void postInterest(SavingsAccount account, boolean postInterestAs,
LocalDate transactionDate, boolean backdatedTxnsAllowedTill);
- // SavingsAccountData postInterest(SavingsAccountData account, boolean
postInterestAs, LocalDate transactionDate,
+ // SavingsAccountData postInterest(SavingsAccountData account, boolean
+ // postInterestAs, LocalDate transactionDate,
// boolean backdatedTxnsAllowedTill);
SavingsAccountData postInterest(SavingsAccountData account, boolean
postInterestAs, LocalDate transactionDate,
@@ -118,4 +119,6 @@ public interface SavingsAccountWritePlatformService {
CommandProcessingResult gsimDeposit(Long gsimId, JsonCommand command);
CommandProcessingResult bulkGSIMClose(Long gsimId, JsonCommand command);
+
+ CommandProcessingResult forceWithdrawal(Long savingsId, JsonCommand
command);
}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/GroupSavingsIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/GroupSavingsIntegrationTest.java
index 75826fda9a..9c8b2d4df7 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/GroupSavingsIntegrationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/GroupSavingsIntegrationTest.java
@@ -1070,6 +1070,7 @@ public class GroupSavingsIntegrationTest {
}
/**
+ *
* Test that duplicate group guarantor detection works and shows proper
error message with group name
*/
@Test
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountForceWithdrawalTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountForceWithdrawalTest.java
new file mode 100644
index 0000000000..d5f2f48b88
--- /dev/null
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/SavingsAccountForceWithdrawalTest.java
@@ -0,0 +1,113 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import org.apache.fineract.client.models.GlobalConfigurationPropertyData;
+import org.apache.fineract.client.models.PostSavingsAccountTransactionsRequest;
+import
org.apache.fineract.client.models.PostSavingsAccountTransactionsResponse;
+import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
+import
org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import
org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
+import
org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+public class SavingsAccountForceWithdrawalTest {
+
+ private ResponseSpecification responseSpec;
+ private RequestSpecification requestSpec;
+ private SavingsProductHelper savingsProductHelper;
+ private SavingsAccountHelper savingsAccountHelper;
+ private GlobalConfigurationHelper globalConfigurationHelper;
+
+ @BeforeEach
+ public void setup() {
+ Utils.initializeRESTAssured();
+ this.requestSpec = new
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+ this.requestSpec.header("Authorization", "Basic " +
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+ this.responseSpec = new
ResponseSpecBuilder().expectStatusCode(200).build();
+ this.savingsAccountHelper = new SavingsAccountHelper(this.requestSpec,
this.responseSpec);
+ this.savingsProductHelper = new SavingsProductHelper();
+ this.globalConfigurationHelper = new GlobalConfigurationHelper();
+ }
+
+ @Test
+ public void testForceWithdrawal() {
+
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT,
+ new PutGlobalConfigurationsRequest().enabled(true));
+
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT_LIMIT,
+ new
PutGlobalConfigurationsRequest().value(5000L).enabled(true));
+
+ GlobalConfigurationPropertyData config = globalConfigurationHelper
+
.getGlobalConfigurationByName(GlobalConfigurationConstants.FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT_LIMIT);
+ Assertions.assertEquals(5000L, config.getValue());
+
+ final Integer clientID = ClientHelper.createClient(this.requestSpec,
this.responseSpec);
+ final Integer savingsProductId = createSavingsProductDailyPosting();
+ final Integer savingsId =
this.savingsAccountHelper.applyForSavingsApplication(clientID,
savingsProductId, "INDIVIDUAL");
+ this.savingsAccountHelper.approveSavings(savingsId);
+ this.savingsAccountHelper.activateSavings(savingsId);
+
+ this.savingsAccountHelper.depositToSavingsAccount(savingsId, "100",
"04 March 2013", null);
+
+ PostSavingsAccountTransactionsRequest request = new
PostSavingsAccountTransactionsRequest() //
+ .locale("en") //
+ .dateFormat("dd MMMM yyyy") //
+ .transactionDate("05 March 2013") //
+ .transactionAmount(java.math.BigDecimal.valueOf(200.0)) //
+ .paymentTypeId(1);
+
+ retrofit2.Response<PostSavingsAccountTransactionsResponse> response =
this.savingsAccountHelper
+ .forceWithdrawalFromSavingsAccount(savingsId.longValue(),
request);
+
+ Assertions.assertTrue(response.isSuccessful(), () -> "Force withdrawal
failed with body: " + getErrorBody(response));
+ }
+
+ private Integer createSavingsProductDailyPosting() {
+ final String savingsProductJSON =
this.savingsProductHelper.withInterestCompoundingPeriodTypeAsDaily()
+
.withInterestPostingPeriodTypeAsDaily().withInterestCalculationPeriodTypeAsDailyBalance().build();
+ return SavingsProductHelper.createSavingsProduct(savingsProductJSON,
requestSpec, responseSpec);
+ }
+
+ private String getErrorBody(retrofit2.Response<?> response) {
+ try {
+ return response.errorBody() != null ?
response.errorBody().string() : "No error body";
+ } catch (Exception e) {
+ return "Failed to read error body: " + e.getMessage();
+ }
+ }
+
+ @AfterEach
+ public void tearDown() {
+
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT,
+ new PutGlobalConfigurationsRequest().enabled(false));
+
globalConfigurationHelper.updateGlobalConfiguration(GlobalConfigurationConstants.FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT_LIMIT,
+ new PutGlobalConfigurationsRequest().value(0L).enabled(false));
+ }
+}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
index cdd1cdf8ae..aa13a2e6fa 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
@@ -36,7 +36,8 @@ public class ExternalEventConfigurationHelper {
+ Utils.TENANT_IDENTIFIER;
// TODO: Rewrite to use fineract-client instead!
- // Example:
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
+ // Example:
+ //
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest)
@Deprecated(forRemoval = true)
public static ArrayList<Map<String, Object>>
getAllExternalEventConfigurations(RequestSpecification requestSpec,
@@ -47,7 +48,8 @@ public class ExternalEventConfigurationHelper {
}
// TODO: Rewrite to use fineract-client instead!
- // Example:
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
+ // Example:
+ //
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest)
@Deprecated(forRemoval = true)
public static ArrayList<Map<String, Object>>
getDefaultExternalEventConfigurations() {
@@ -667,11 +669,17 @@ public class ExternalEventConfigurationHelper {
savingsAccountsStayedLockedBusinessEvent.put("enabled", false);
defaults.add(savingsAccountsStayedLockedBusinessEvent);
+ Map<String, Object> savingsAccountForceWithdrawalBusinessEvent = new
HashMap<>();
+ savingsAccountForceWithdrawalBusinessEvent.put("type",
"SavingsAccountForceWithdrawalBusinessEvent");
+ savingsAccountForceWithdrawalBusinessEvent.put("enabled", false);
+ defaults.add(savingsAccountForceWithdrawalBusinessEvent);
+
return defaults;
}
// TODO: Rewrite to use fineract-client instead!
- // Example:
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
+ // Example:
+ //
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest)
@Deprecated(forRemoval = true)
public static String getExternalEventConfigurationsForUpdateJSON() {
@@ -690,7 +698,8 @@ public class ExternalEventConfigurationHelper {
}
// TODO: Rewrite to use fineract-client instead!
- // Example:
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
+ // Example:
+ //
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest)
@Deprecated(forRemoval = true)
public static Map<String, Boolean>
updateExternalEventConfigurations(RequestSpecification requestSpec,
@@ -701,7 +710,8 @@ public class ExternalEventConfigurationHelper {
}
// TODO: Rewrite to use fineract-client instead!
- // Example:
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
+ // Example:
+ //
org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest)
@Deprecated(forRemoval = true)
public static void resetDefaultConfigurations(RequestSpecification
requestSpec, ResponseSpecification responseSpec) {
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java
index 3b42d373e4..63ff5a9fbe 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java
@@ -58,7 +58,8 @@ public class GlobalConfigurationHelper {
return
Calls.ok(FineractClientHelper.getFineractClient().globalConfigurations.retrieveOne3(configId));
}
- // TODO: This is quite a bad pattern and adds a lot of time to individual
test executions
+ // TODO: This is quite a bad pattern and adds a lot of time to individual
test
+ // executions
public void resetAllDefaultGlobalConfigurations() {
GetGlobalConfigurationsResponse actualGlobalConfigurations =
getAllGlobalConfigurations();
@@ -607,6 +608,20 @@ public class GlobalConfigurationHelper {
enableOriginatorCreationDuringLoanApplication.put("trapDoor", false);
defaults.add(enableOriginatorCreationDuringLoanApplication);
+ HashMap<String, Object> forceWithdrawalOnSavingsAccount = new
HashMap<>();
+ forceWithdrawalOnSavingsAccount.put("name",
GlobalConfigurationConstants.FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT);
+ forceWithdrawalOnSavingsAccount.put("value", 0L);
+ forceWithdrawalOnSavingsAccount.put("enabled", false);
+ forceWithdrawalOnSavingsAccount.put("trapDoor", false);
+ defaults.add(forceWithdrawalOnSavingsAccount);
+
+ HashMap<String, Object> forceWithdrawalOnSavingsAccountLimit = new
HashMap<>();
+ forceWithdrawalOnSavingsAccountLimit.put("name",
GlobalConfigurationConstants.FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT_LIMIT);
+ forceWithdrawalOnSavingsAccountLimit.put("value", 0L);
+ forceWithdrawalOnSavingsAccountLimit.put("enabled", false);
+ forceWithdrawalOnSavingsAccountLimit.put("trapDoor", false);
+ defaults.add(forceWithdrawalOnSavingsAccountLimit);
+
return defaults;
}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java
index 1fe7e77b82..9431c96b2e 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/savings/SavingsAccountHelper.java
@@ -84,6 +84,7 @@ public class SavingsAccountHelper {
private static final String DEPOSIT_SAVINGS_COMMAND = "deposit";
private static final String WITHDRAW_SAVINGS_COMMAND = "withdrawal";
+ private static final String FORCE_WITHDRAW_SAVINGS_COMMAND =
"force-withdrawal";
private static final String GSIM_SAVINGS = "/gsim";
private static final String GSIM_SAVINGS_COMMAND = "/gsimcommands";
private static final String GSIM_DEPOSIT_SAVINGS_COMMAND = "gsimDeposit";
@@ -434,6 +435,12 @@ public class SavingsAccountHelper {
return
Calls.executeU(FineractClientHelper.getFineractClient().savingsTransactions.transaction2(savingsId,
request, "withdrawal"));
}
+ public Response<PostSavingsAccountTransactionsResponse>
forceWithdrawalFromSavingsAccount(final Long savingsId,
+ PostSavingsAccountTransactionsRequest request) {
+ return Calls.executeU(
+
FineractClientHelper.getFineractClient().savingsTransactions.transaction2(savingsId,
request, "force-withdrawal"));
+ }
+
public Response<PostSavingsAccountTransactionsResponse>
depositIntoSavingsAccount(final Long savingsId,
PostSavingsAccountTransactionsRequest request) {
return
Calls.executeU(FineractClientHelper.getFineractClient().savingsTransactions.transaction2(savingsId,
request, "deposit"));
@@ -1524,4 +1531,11 @@ public class SavingsAccountHelper {
return total.setScale(2, java.math.RoundingMode.HALF_UP);
}
+ public Object forceWithdrawalFromSavingsAccount(final Integer savingsId,
final String amount, String date,
+ String jsonAttributeToGetback) {
+ LOG.info("\n--------------------------------- SAVINGS TRANSACTION
FORCE WITHDRAWAL --------------------------------");
+ return
performSavingActions(createSavingsTransactionURL("force-withdrawal",
savingsId), getSavingsTransactionJSON(amount, date),
+ jsonAttributeToGetback);
+ }
+
}