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);
+    }
+
 }

Reply via email to