Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ licenses
**/images/diag-*.svg

fineract-provider/src/main/generated/
**/.apt_generated/

**/out/
gradleExp/

.run/
.java-version

.windsurf/
.windsurf/
Original file line number Diff line number Diff line change
Expand Up @@ -1620,6 +1620,15 @@ public CommandWrapperBuilder savingsAccountWithdrawal(final Long accountId) {
return this;
}

public CommandWrapperBuilder savingsAccountForceWithdrawal(final Long savingsId) {
this.actionName = "FORCE_WITHDRAWAL";
this.entityName = "SAVINGSACCOUNT";
this.entityId = savingsId;
this.savingsId = savingsId;
this.href = "/savingsaccounts/" + savingsId + "/transactions?command=force-withdrawal";
return this;
}

public CommandWrapperBuilder undoSavingsAccountTransaction(final Long accountId, final Long transactionId) {
this.actionName = "UNDOTRANSACTION";
this.entityName = "SAVINGSACCOUNT";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ public final class GlobalConfigurationConstants {
public static final String ALLOWED_LOAN_STATUSES_FOR_EXTERNAL_ASSET_TRANSFER = "allowed-loan-statuses-for-external-asset-transfer";
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 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";

private GlobalConfigurationConstants() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,8 @@ public interface ConfigurationDomainService {
boolean isImmediateChargeAccrualPostMaturityEnabled();

String getAssetOwnerTransferOustandingInterestStrategy();

boolean isForceWithdrawalOnSavingsAccountEnabled();

Long retrieveForceWithdrawalOnSavingsAccountLimit();
}
Original file line number Diff line number Diff line change
Expand Up @@ -548,4 +548,14 @@ public String getAssetOwnerTransferOustandingInterestStrategy() {
return getGlobalConfigurationPropertyData(
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
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;
Expand Down Expand Up @@ -141,6 +142,7 @@ public class InteropServiceImpl implements InteropService {

private final DefaultToApiJsonSerializer<LoanAccountData> toApiJsonSerializer;
private final DatabaseSpecificSQLGenerator sqlGenerator;
private final ConfigurationDomainService configurationDomainService;

private static final class KycMapper implements RowMapper<InteropKycData> {

Expand Down Expand Up @@ -566,7 +568,7 @@ private Loan validateAndGetLoan(String accountId) {
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())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,10 +55,11 @@ public InteropService interopService(PlatformSecurityContext securityContext, In
SavingsAccountTransactionSummaryWrapper savingsAccountTransactionSummaryWrapper,
SavingsAccountDomainService savingsAccountService, JdbcTemplate jdbcTemplate,
PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService,
DefaultToApiJsonSerializer<LoanAccountData> toApiJsonSerializer, DatabaseSpecificSQLGenerator sqlGenerator) {
DefaultToApiJsonSerializer<LoanAccountData> toApiJsonSerializer, DatabaseSpecificSQLGenerator sqlGenerator,
ConfigurationDomainService configurationDomainService) {
return new InteropServiceImpl(securityContext, interopDataValidator, savingsAccountRepository, savingsAccountTransactionRepository,
applicationCurrencyRepository, noteRepository, paymentTypeRepository, identifierRepository, loanRepositoryWrapper,
savingsHelper, savingsAccountTransactionSummaryWrapper, savingsAccountService, jdbcTemplate,
commandsSourceWritePlatformService, toApiJsonSerializer, sqlGenerator);
commandsSourceWritePlatformService, toApiJsonSerializer, sqlGenerator, configurationDomainService);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ public String transaction(@PathParam("savingsId") final Long savingsId, @QueryPa
} else if (is(commandParam, "withdrawal")) {
final CommandWrapper commandRequest = builder.savingsAccountWithdrawal(savingsId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "force-withdrawal")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This else if is becoming too long i think we can optimise this further

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This else if is becoming too long i think we can optimise this further

@Aman-Mittal Valid point. This class definitely relies heavily on the else-if chain pattern.

However, for this specific PR, I intentionally stuck to the existing pattern to ensure consistency with the surrounding code and to strictly limit the sco0pe of changes to the "Force Withdrawal" feature.

I am hesitant to refactor the command dispatch logic here as it would require touching legacy paths (standard withdrawal/deposit) which increases regression risk. I think a structural refactor would be better suited for a dedicated "Technical Debt" PR.

Does that sound reasonable?

final CommandWrapper commandRequest = builder.savingsAccountForceWithdrawal(savingsId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
} else if (is(commandParam, "postInterestAsOn")) {
final CommandWrapper commandRequest = builder.savingsAccountInterestPosting(savingsId).build();
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
Expand All @@ -200,7 +203,7 @@ public String transaction(@PathParam("savingsId") final Long savingsId, @QueryPa
if (result == null) {
//
throw new UnrecognizedQueryParamException("command", commandParam,
new Object[] { "deposit", "withdrawal", SavingsApiConstants.COMMAND_HOLD_AMOUNT });
new Object[] { "deposit", "withdrawal", "force-withdrawal", SavingsApiConstants.COMMAND_HOLD_AMOUNT });
}

return this.toApiJsonSerializer.serialize(result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
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;
Expand Down Expand Up @@ -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,
Expand All @@ -130,7 +132,8 @@ public DepositAccountAssembler(final SavingsAccountTransactionSummaryWrapper sav
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;
Expand All @@ -146,6 +149,7 @@ public DepositAccountAssembler(final SavingsAccountTransactionSummaryWrapper sav
this.context = context;
this.paymentDetailAssembler = paymentDetailAssembler;
this.externalIdFactory = externalIdFactory;
this.configurationDomainService = configurationDomainService;
}

/**
Expand Down Expand Up @@ -356,7 +360,7 @@ public SavingsAccount assembleFrom(final JsonCommand command, final AppUser subm
}

if (account != null) {
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper, this.configurationDomainService);
account.validateNewApplicationState(depositAccountType.resourceName());
}

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

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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ public SavingsAccount assembleFrom(final JsonCommand command, final AppUser subm
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);

Expand Down Expand Up @@ -381,7 +381,7 @@ public SavingsAccount loadTransactionsToSavingsAccount(final SavingsAccount acco
}
}

account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper, this.configurationDomainService);
return account;
}

Expand Down Expand Up @@ -421,7 +421,7 @@ public boolean isRelaxingDaysConfigForPivotDateEnabled() {
}

public void setHelpers(final SavingsAccount account) {
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper);
account.setHelpers(this.savingsAccountTransactionSummaryWrapper, this.savingsHelper, this.configurationDomainService);
}

/**
Expand Down Expand Up @@ -465,7 +465,7 @@ public SavingsAccount assembleFrom(final Client client, final Group group, final
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);

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

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account,
}

account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(),
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill);
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill, transactionBooleanValues.isForceWithdrawal());

saveTransactionToGenerateTransactionId(withdrawal);
if (backdatedTxnsAllowedTill) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,69 @@ public CommandProcessingResult withdrawal(final Long savingsId, final JsonComman
.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) {
Expand Down
Loading
Loading