Skip to content

Commit e3ad1fb

Browse files
committed
FINERACT-2471: Phase 1: Implement 'Force Withdrawal' for Savings Accounts
This PR implements Phase 1 of the Force Withdrawal feature for Savings Accounts. Changes include: - New force-withdrawal API endpoint at POST /savingsaccounts/{savingsId}/transactions?command=force-withdrawal - Two new global configuration flags: * force-withdrawal-on-savings-account: Enable/disable the feature * force-withdrawal-on-savings-account-limit: Maximum amount allowed for force withdrawal - ForceWithdrawalSavingsAccountCommandHandler for command processing - SavingsTransactionBooleanValues extended with isForceWithdrawal flag - Integration tests for the force withdrawal functionality The force withdrawal bypasses balance validation, allowing withdrawals even when the account has insufficient funds, respecting the configured limit. Fix: Add missing FORCE_WITHDRAWAL CHECKER permission
1 parent 28f63cc commit e3ad1fb

File tree

21 files changed

+428
-26
lines changed

21 files changed

+428
-26
lines changed

fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,6 +1620,15 @@ public CommandWrapperBuilder savingsAccountWithdrawal(final Long accountId) {
16201620
return this;
16211621
}
16221622

1623+
public CommandWrapperBuilder savingsAccountForceWithdrawal(final Long savingsId) {
1624+
this.actionName = "FORCE_WITHDRAWAL";
1625+
this.entityName = "SAVINGSACCOUNT";
1626+
this.entityId = savingsId;
1627+
this.savingsId = savingsId;
1628+
this.href = "/savingsaccounts/" + savingsId + "?command=forceWithdrawal";
1629+
return this;
1630+
}
1631+
16231632
public CommandWrapperBuilder undoSavingsAccountTransaction(final Long accountId, final Long transactionId) {
16241633
this.actionName = "UNDOTRANSACTION";
16251634
this.entityName = "SAVINGSACCOUNT";

fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/api/GlobalConfigurationConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public final class GlobalConfigurationConstants {
8080
public static final String ALLOWED_LOAN_STATUSES_FOR_EXTERNAL_ASSET_TRANSFER = "allowed-loan-statuses-for-external-asset-transfer";
8181
public static final String ALLOWED_LOAN_STATUSES_OF_DELAYED_SETTLEMENT_FOR_EXTERNAL_ASSET_TRANSFER = "allowed-loan-statuses-of-delayed-settlement-for-external-asset-transfer";
8282
public static final String ENABLE_ORIGINATOR_CREATION_DURING_LOAN_APPLICATION = "enable-originator-creation-during-loan-application";
83+
public static final String FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT = "allow-force-withdrawal-on-savings-account";
84+
public static final String FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT_LIMIT = "force-withdrawal-on-savings-account-limit";
8385

8486
private GlobalConfigurationConstants() {}
8587
}

fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,8 @@ public interface ConfigurationDomainService {
151151
boolean isImmediateChargeAccrualPostMaturityEnabled();
152152

153153
String getAssetOwnerTransferOustandingInterestStrategy();
154+
155+
boolean isForceWithdrawalOnSavingsAccountEnabled();
156+
157+
Long retrieveForceWithdrawalOnSavingsAccountLimit();
154158
}

fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,4 +548,14 @@ public String getAssetOwnerTransferOustandingInterestStrategy() {
548548
return getGlobalConfigurationPropertyData(
549549
GlobalConfigurationConstants.ASSET_OWNER_TRANSFER_OUTSTANDING_INTEREST_CALCULATION_STRATEGY).getStringValue();
550550
}
551+
552+
@Override
553+
public boolean isForceWithdrawalOnSavingsAccountEnabled() {
554+
return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT).isEnabled();
555+
}
556+
557+
@Override
558+
public Long retrieveForceWithdrawalOnSavingsAccountLimit() {
559+
return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.FORCE_WITHDRAWAL_ON_SAVINGS_ACCOUNT_LIMIT).getValue();
560+
}
551561
}

fineract-provider/src/main/java/org/apache/fineract/interoperation/service/InteropServiceImpl.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.apache.fineract.commands.domain.CommandWrapper;
4545
import org.apache.fineract.commands.service.CommandWrapperBuilder;
4646
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
47+
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
4748
import org.apache.fineract.infrastructure.core.api.JsonCommand;
4849
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
4950
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
@@ -141,6 +142,7 @@ public class InteropServiceImpl implements InteropService {
141142

142143
private final DefaultToApiJsonSerializer<LoanAccountData> toApiJsonSerializer;
143144
private final DatabaseSpecificSQLGenerator sqlGenerator;
145+
private final ConfigurationDomainService configurationDomainService;
144146

145147
private static final class KycMapper implements RowMapper<InteropKycData> {
146148

@@ -566,7 +568,7 @@ private Loan validateAndGetLoan(String accountId) {
566568
private SavingsAccount validateAndGetSavingAccount(@NonNull InteropRequestData request) {
567569
// TODO: error handling
568570
SavingsAccount savingsAccount = validateAndGetSavingAccount(request.getAccountId());
569-
savingsAccount.setHelpers(savingsAccountTransactionSummaryWrapper, savingsHelper);
571+
savingsAccount.setHelpers(savingsAccountTransactionSummaryWrapper, savingsHelper, configurationDomainService);
570572

571573
ApplicationCurrency requestCurrency = currencyRepository.findOneByCode(request.getAmount().getCurrency());
572574
if (!savingsAccount.getCurrency().getCode().equals(requestCurrency.getCode())) {

fineract-provider/src/main/java/org/apache/fineract/interoperation/starter/InteroperationConfiguration.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.apache.fineract.interoperation.starter;
2020

2121
import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService;
22+
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
2223
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
2324
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
2425
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
@@ -54,10 +55,11 @@ public InteropService interopService(PlatformSecurityContext securityContext, In
5455
SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper,
5556
SavingsAccountDomainService savingsAccountService, JdbcTemplate jdbcTemplate,
5657
PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService,
57-
DefaultToApiJsonSerializer<LoanAccountData> toApiJsonSerializer, DatabaseSpecificSQLGenerator sqlGenerator) {
58+
DefaultToApiJsonSerializer<LoanAccountData> toApiJsonSerializer, DatabaseSpecificSQLGenerator sqlGenerator,
59+
ConfigurationDomainService configurationDomainService) {
5860
return new InteropServiceImpl(securityContext, interopDataValidator, savingsAccountRepository, savingsAccountTransactionRepository,
5961
applicationCurrencyRepository, noteRepository, paymentTypeRepository, identifierRepository, loanRepositoryWrapper,
6062
savingsHelper, savingsAccountTransactionSummaryWrapper, savingsAccountService, jdbcTemplate,
61-
commandsSourceWritePlatformService, toApiJsonSerializer, sqlGenerator);
63+
commandsSourceWritePlatformService, toApiJsonSerializer, sqlGenerator, configurationDomainService);
6264
}
6365
}

fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/SavingsAccountTransactionsApiResource.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,9 @@ public String transaction(@PathParam("savingsId") final Long savingsId, @QueryPa
193193
} else if (is(commandParam, "withdrawal")) {
194194
final CommandWrapper commandRequest = builder.savingsAccountWithdrawal(savingsId).build();
195195
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
196+
} else if (is(commandParam, "force-withdrawal")) {
197+
final CommandWrapper commandRequest = builder.savingsAccountForceWithdrawal(savingsId).build();
198+
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
196199
} else if (is(commandParam, "postInterestAsOn")) {
197200
final CommandWrapper commandRequest = builder.savingsAccountInterestPosting(savingsId).build();
198201
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
@@ -204,7 +207,7 @@ public String transaction(@PathParam("savingsId") final Long savingsId, @QueryPa
204207
if (result == null) {
205208
//
206209
throw new UnrecognizedQueryParamException("command", commandParam,
207-
new Object[] { "deposit", "withdrawal", SavingsApiConstants.COMMAND_HOLD_AMOUNT });
210+
new Object[] { "deposit", "withdrawal", "force-withdrawal", SavingsApiConstants.COMMAND_HOLD_AMOUNT });
208211
}
209212

210213
return this.toApiJsonSerializer.serialize(result);

fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/DepositAccountAssembler.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import java.util.Locale;
6666
import java.util.Set;
6767
import org.apache.commons.lang3.StringUtils;
68+
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
6869
import org.apache.fineract.infrastructure.core.api.JsonCommand;
6970
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
7071
import org.apache.fineract.infrastructure.core.exception.UnsupportedParameterException;
@@ -120,6 +121,7 @@ public class DepositAccountAssembler {
120121
private final PaymentDetailAssembler paymentDetailAssembler;
121122

122123
private final ExternalIdFactory externalIdFactory;
124+
private final ConfigurationDomainService configurationDomainService;
123125

124126
@Autowired
125127
public DepositAccountAssembler(final SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper,
@@ -130,7 +132,8 @@ public DepositAccountAssembler(final SavingsAccountTransactionSummaryWrapper sav
130132
final DepositProductAssembler depositProductAssembler,
131133
final RecurringDepositProductRepository recurringDepositProductRepository,
132134
final AccountTransfersReadPlatformService accountTransfersReadPlatformService, final PlatformSecurityContext context,
133-
final PaymentDetailAssembler paymentDetailAssembler, ExternalIdFactory externalIdFactory) {
135+
final PaymentDetailAssembler paymentDetailAssembler, ExternalIdFactory externalIdFactory,
136+
final ConfigurationDomainService configurationDomainService) {
134137

135138
this.savingsAccountTransactionSummaryWrapper = savingsAccountTransactionSummaryWrapper;
136139
this.clientRepository = clientRepository;
@@ -146,6 +149,7 @@ public DepositAccountAssembler(final SavingsAccountTransactionSummaryWrapper sav
146149
this.context = context;
147150
this.paymentDetailAssembler = paymentDetailAssembler;
148151
this.externalIdFactory = externalIdFactory;
152+
this.configurationDomainService = configurationDomainService;
149153
}
150154

151155
/**
@@ -356,7 +360,7 @@ public SavingsAccount assembleFrom(final JsonCommand command, final AppUser subm
356360
}
357361

358362
if (account != null) {
359-
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
363+
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper, this.configurationDomainService);
360364
account.validateNewApplicationState(depositAccountType.resourceName());
361365
}
362366

@@ -365,12 +369,12 @@ public SavingsAccount assembleFrom(final JsonCommand command, final AppUser subm
365369

366370
public SavingsAccount assembleFrom(final Long savingsId, DepositAccountType depositAccountType) {
367371
final SavingsAccount account = this.savingsAccountRepository.findOneWithNotFoundDetection(savingsId, depositAccountType);
368-
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
372+
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper, this.configurationDomainService);
369373
return account;
370374
}
371375

372376
public void assignSavingAccountHelpers(final SavingsAccount savingsAccount) {
373-
savingsAccount.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
377+
savingsAccount.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper, this.configurationDomainService);
374378
}
375379

376380
public DepositAccountTermAndPreClosure assembleAccountTermAndPreClosure(final JsonCommand command,

fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountAssembler.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ public SavingsAccount assembleFrom(final JsonCommand command, final AppUser subm
330330
minRequiredOpeningBalance, lockinPeriodFrequency, lockinPeriodFrequencyType, iswithdrawalFeeApplicableForTransfer, charges,
331331
allowOverdraft, overdraftLimit, enforceMinRequiredBalance, minRequiredBalance, maxAllowedLienLimit, lienAllowed,
332332
nominalAnnualInterestRateOverdraft, minOverdraftForInterestCalculation, withHoldTax);
333-
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
333+
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper, this.configurationDomainService);
334334

335335
account.validateNewApplicationState(SAVINGS_ACCOUNT_RESOURCE_NAME);
336336

@@ -381,7 +381,7 @@ public SavingsAccount loadTransactionsToSavingsAccount(final SavingsAccount acco
381381
}
382382
}
383383

384-
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
384+
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper, this.configurationDomainService);
385385
return account;
386386
}
387387

@@ -421,7 +421,7 @@ public boolean isRelaxingDaysConfigForPivotDateEnabled() {
421421
}
422422

423423
public void setHelpers(final SavingsAccount account) {
424-
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
424+
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper, this.configurationDomainService);
425425
}
426426

427427
/**
@@ -465,7 +465,7 @@ public SavingsAccount assembleFrom(final Client client, final Group group, final
465465
product.isMinRequiredBalanceEnforced(), product.minRequiredBalance(), product.maxAllowedLienLimit(),
466466
product.isLienAllowed(), product.nominalAnnualInterestRateOverdraft(), product.minOverdraftForInterestCalculation(),
467467
product.withHoldTax());
468-
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
468+
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper, this.configurationDomainService);
469469

470470
account.validateNewApplicationState(SAVINGS_ACCOUNT_RESOURCE_NAME);
471471

@@ -475,7 +475,7 @@ public SavingsAccount assembleFrom(final Client client, final Group group, final
475475
}
476476

477477
public void assignSavingAccountHelpers(final SavingsAccount savingsAccount) {
478-
savingsAccount.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
478+
savingsAccount.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper, this.configurationDomainService);
479479
}
480480

481481
public void assignSavingAccountHelpers(final SavingsAccountData savingsAccountData) {

fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account,
130130
}
131131

132132
account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(),
133-
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill);
133+
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill, transactionBooleanValues.isForceWithdrawal());
134134

135135
saveTransactionToGenerateTransactionId(withdrawal);
136136
if (backdatedTxnsAllowedTill) {

0 commit comments

Comments
 (0)