feat: implement full current account transaction write service

- New CurrentAccountTransactionWritePlatformService interface with deposit,
  withdrawal, holdAmount, releaseAmount, undoTransaction, reverseTransaction,
  and adjustTransaction operations
- New CurrentAccountTransactionWritePlatformServiceImpl using m_current_account
  tables directly (not m_savings_account), with GL journal entry posting
- Add holdAmount, releaseAmount, reversal factory methods to CurrentAccountTransaction
- Wire all 7 current account transaction command handlers to new write service
- Fix CurrentAccountTransactionsApiResource template SQL column: currency_multiplesof
- Fix account number search in SearchParameters

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-05-21 11:20:30 +05:30
co-authored by Claude Sonnet 4.6
parent b25d5a30d2
commit ce0fb18eeb
21 changed files with 6961 additions and 6252 deletions
@@ -44,6 +44,7 @@ public class SearchParameters {
private String orderBy; private String orderBy;
private String sortOrder; private String sortOrder;
private String accountNo; private String accountNo;
private Integer depositType;
private String currencyCode; private String currencyCode;
private Long staffId; private Long staffId;
private Long loanId; private Long loanId;
@@ -146,4 +146,48 @@ public class CurrentAccountTransaction extends AbstractPersistableCustom<Long> {
txn.submittedOnDate = transactionDate; txn.submittedOnDate = transactionDate;
return txn; return txn;
} }
// transactionTypeEnum = 8 (Hold Amount)
public static CurrentAccountTransaction holdAmount(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate, String reasonForBlock) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
txn.currentAccount = account;
txn.office = office;
txn.transactionTypeEnum = 8;
txn.amount = amount;
txn.transactionDate = transactionDate;
txn.lienTransaction = true;
txn.reasonForBlock = reasonForBlock;
txn.submittedOnDate = transactionDate;
return txn;
}
// transactionTypeEnum = 9 (Release Amount)
public static CurrentAccountTransaction releaseAmount(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate, Long holdTransactionId) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
txn.currentAccount = account;
txn.office = office;
txn.transactionTypeEnum = 9;
txn.amount = amount;
txn.transactionDate = transactionDate;
txn.releaseIdOfHoldAmountTransaction = holdTransactionId;
txn.submittedOnDate = transactionDate;
return txn;
}
// transactionTypeEnum = 7 (Reversal / Undo)
public static CurrentAccountTransaction reversal(CurrentAccount account, Office office,
BigDecimal amount, LocalDate transactionDate, Long originalTransactionId) {
CurrentAccountTransaction txn = new CurrentAccountTransaction();
txn.currentAccount = account;
txn.office = office;
txn.transactionTypeEnum = 7;
txn.amount = amount;
txn.transactionDate = transactionDate;
txn.reversal = true;
txn.originalTransactionId = originalTransactionId;
txn.submittedOnDate = transactionDate;
return txn;
}
} }
@@ -26,4 +26,6 @@ public interface CurrentAccountReadPlatformService {
Map<String, Object> retrieveOne(Long accountId); Map<String, Object> retrieveOne(Long accountId);
List<Map<String, Object>> retrieveAll(); List<Map<String, Object>> retrieveAll();
List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName);
} }
@@ -0,0 +1,39 @@
/**
* 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.currentaccount.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
public interface CurrentAccountTransactionWritePlatformService {
CommandProcessingResult deposit(Long accountId, JsonCommand command);
CommandProcessingResult withdrawal(Long accountId, JsonCommand command);
CommandProcessingResult holdAmount(Long accountId, JsonCommand command);
CommandProcessingResult releaseAmount(Long accountId, Long transactionId);
CommandProcessingResult undoTransaction(Long accountId, Long transactionId, boolean isReversal);
CommandProcessingResult reverseTransaction(Long accountId, Long transactionId, boolean isReversal, JsonCommand command);
CommandProcessingResult adjustTransaction(Long accountId, Long transactionId, JsonCommand command);
}
@@ -18,6 +18,8 @@
*/ */
package org.apache.fineract.portfolio.savings.api; package org.apache.fineract.portfolio.savings.api;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponse;
@@ -32,6 +34,10 @@ import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import java.time.LocalDate;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.domain.CommandWrapper;
@@ -43,10 +49,14 @@ import org.apache.fineract.infrastructure.core.exception.UnrecognizedQueryParamE
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings; import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotFoundException;
import org.apache.fineract.portfolio.paymenttype.data.PaymentTypeData;
import org.apache.fineract.portfolio.paymenttype.service.PaymentTypeReadService;
import org.apache.fineract.portfolio.savings.DepositsApiConstants; import org.apache.fineract.portfolio.savings.DepositsApiConstants;
import org.apache.fineract.portfolio.savings.DepositAccountType; import org.apache.fineract.portfolio.savings.DepositAccountType;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionData;
import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService; import org.apache.fineract.portfolio.savings.service.SavingsAccountReadPlatformService;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Path("/v1/currentaccounts/{accountId}/transactions") @Path("/v1/currentaccounts/{accountId}/transactions")
@@ -55,11 +65,58 @@ import org.springframework.stereotype.Component;
@RequiredArgsConstructor @RequiredArgsConstructor
public class CurrentAccountTransactionsApiResource { public class CurrentAccountTransactionsApiResource {
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final DefaultToApiJsonSerializer<SavingsAccountTransactionData> toApiJsonSerializer; private final DefaultToApiJsonSerializer<SavingsAccountTransactionData> toApiJsonSerializer;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
private final ApiRequestParameterHelper apiRequestParameterHelper; private final ApiRequestParameterHelper apiRequestParameterHelper;
private final SavingsAccountReadPlatformService savingsAccountReadPlatformService; private final SavingsAccountReadPlatformService savingsAccountReadPlatformService;
private final PaymentTypeReadService paymentTypeReadService;
private final JdbcTemplate jdbcTemplate;
@GET
@Path("template")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve current account transaction template")
@ApiResponse(responseCode = "200", description = "OK")
public String retrieveTemplate(@PathParam("accountId") final Long accountId, @Context final UriInfo uriInfo) {
context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME);
final String sql = "SELECT ca.currency_code, ca.currency_digits, ca.currency_multiplesof, "
+ "mc.display_symbol, mc.name AS currency_name, mc.internationalized_name_code "
+ "FROM m_current_account ca "
+ "LEFT JOIN m_currency mc ON mc.code = ca.currency_code "
+ "WHERE ca.id = ?";
final Map<String, Object> template;
try {
template = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> {
final Map<String, Object> map = new LinkedHashMap<>();
final LocalDate today = LocalDate.now();
map.put("date", new int[] { today.getYear(), today.getMonthValue(), today.getDayOfMonth() });
final Map<String, Object> currency = new LinkedHashMap<>();
currency.put("code", rs.getString("currency_code"));
currency.put("decimalPlaces", rs.getInt("currency_digits"));
currency.put("inMultiplesOf", rs.getInt("currency_multiplesof"));
currency.put("displaySymbol", rs.getString("display_symbol"));
currency.put("name", rs.getString("currency_name"));
currency.put("nameCode", rs.getString("internationalized_name_code"));
map.put("currency", currency);
return map;
}, accountId);
} catch (org.springframework.dao.EmptyResultDataAccessException e) {
throw new CurrentAccountNotFoundException(accountId);
}
final Collection<PaymentTypeData> paymentTypeOptions = paymentTypeReadService.retrieveAllPaymentTypes();
template.put("paymentTypeOptions", paymentTypeOptions);
return GSON.toJson(template);
}
@GET @GET
@Path("{transactionId}") @Path("{transactionId}")
@@ -101,10 +101,12 @@ public class CurrentAccountsApiResource {
@QueryParam("offset") final Integer offset, @QueryParam("offset") final Integer offset,
@QueryParam("limit") final Integer limit, @QueryParam("limit") final Integer limit,
@QueryParam("orderBy") final String orderBy, @QueryParam("orderBy") final String orderBy,
@QueryParam("sortOrder") final String sortOrder) { @QueryParam("sortOrder") final String sortOrder,
@QueryParam("accountNo") @Parameter(description = "accountNo") final String accountNo,
@QueryParam("clientName") @Parameter(description = "clientName") final String clientName) {
context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME); context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.CURRENT_ACCOUNT_RESOURCE_NAME);
final List<Map<String, Object>> accounts = currentAccountReadPlatformService.retrieveAll(); final List<Map<String, Object>> accounts = currentAccountReadPlatformService.retrieveAll(offset, limit, accountNo, clientName);
return GSON.toJson(accounts); return GSON.toJson(accounts);
} }
@@ -159,7 +159,9 @@ public class FixedDepositAccountsApiResource {
@QueryParam("offset") @Parameter(description = "offset") final Integer offset, @QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit, @QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) { @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("accountNo") @Parameter(description = "accountNo") final String accountNo,
@QueryParam("clientName") @Parameter(description = "clientName") final String clientName) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME); this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESOURCE_NAME);
sqlValidator.validate(orderBy); sqlValidator.validate(orderBy);
@@ -170,7 +172,7 @@ public class FixedDepositAccountsApiResource {
if (paginationParameters.isPaged()) { if (paginationParameters.isPaged()) {
final Page<DepositAccountData> account = this.depositAccountReadPlatformService final Page<DepositAccountData> account = this.depositAccountReadPlatformService
.retrieveAllPaged(DepositAccountType.FIXED_DEPOSIT, paginationParameters); .retrieveAllPaged(DepositAccountType.FIXED_DEPOSIT, paginationParameters, accountNo, clientName);
return this.toApiJsonSerializer.serialize(settings, account, return this.toApiJsonSerializer.serialize(settings, account,
DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); DepositsApiConstants.FIXED_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
} }
@@ -140,7 +140,9 @@ public class RecurringDepositAccountsApiResource {
@QueryParam("offset") @Parameter(description = "offset") final Integer offset, @QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit, @QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) { @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("accountNo") @Parameter(description = "accountNo") final String accountNo,
@QueryParam("clientName") @Parameter(description = "clientName") final String clientName) {
this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME); this.context.authenticatedUser().validateHasReadPermission(DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESOURCE_NAME);
sqlValidator.validate(orderBy); sqlValidator.validate(orderBy);
@@ -151,7 +153,7 @@ public class RecurringDepositAccountsApiResource {
if (paginationParameters.isPaged()) { if (paginationParameters.isPaged()) {
final Page<DepositAccountData> account = this.depositAccountReadPlatformService final Page<DepositAccountData> account = this.depositAccountReadPlatformService
.retrieveAllPaged(DepositAccountType.RECURRING_DEPOSIT, paginationParameters); .retrieveAllPaged(DepositAccountType.RECURRING_DEPOSIT, paginationParameters, accountNo, clientName);
return this.toApiJsonSerializer.serialize(settings, account, return this.toApiJsonSerializer.serialize(settings, account,
DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS); DepositsApiConstants.RECURRING_DEPOSIT_ACCOUNT_RESPONSE_DATA_PARAMETERS);
} }
@@ -130,7 +130,10 @@ public class SavingsAccountsApiResource {
@QueryParam("offset") @Parameter(description = "offset") final Integer offset, @QueryParam("offset") @Parameter(description = "offset") final Integer offset,
@QueryParam("limit") @Parameter(description = "limit") final Integer limit, @QueryParam("limit") @Parameter(description = "limit") final Integer limit,
@QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy, @QueryParam("orderBy") @Parameter(description = "orderBy") final String orderBy,
@QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder) { @QueryParam("sortOrder") @Parameter(description = "sortOrder") final String sortOrder,
@QueryParam("accountNo") @Parameter(description = "accountNo") final String accountNo,
@QueryParam("clientName") @Parameter(description = "clientName") final String clientName,
@QueryParam("depositType") @Parameter(description = "depositType") final Integer depositType) {
context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
@@ -138,7 +141,7 @@ public class SavingsAccountsApiResource {
sqlValidator.validate(sortOrder); sqlValidator.validate(sortOrder);
sqlValidator.validate(externalId); sqlValidator.validate(externalId);
final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset) final SearchParameters searchParameters = SearchParameters.builder().limit(limit).externalId(externalId).offset(offset)
.orderBy(orderBy).sortOrder(sortOrder).build(); .orderBy(orderBy).sortOrder(sortOrder).accountNo(accountNo).name(clientName).depositType(depositType).build();
final Page<SavingsAccountData> products = savingsAccountReadPlatformService.retrieveAll(searchParameters); final Page<SavingsAccountData> products = savingsAccountReadPlatformService.retrieveAll(searchParameters);
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "ADJUSTTRANSACTION") @CommandType(entity = "CURRENTACCOUNT", action = "ADJUSTTRANSACTION")
public class CurrentAccountTransactionAdjustmentCommandHandler implements NewCommandSourceHandler { public class CurrentAccountTransactionAdjustmentCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService; private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired @Autowired
public CurrentAccountTransactionAdjustmentCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { public CurrentAccountTransactionAdjustmentCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService; this.writePlatformService = writePlatformService;
} }
@@ -42,6 +42,6 @@ public class CurrentAccountTransactionAdjustmentCommandHandler implements NewCom
@Override @Override
public CommandProcessingResult processCommand(final JsonCommand command) { public CommandProcessingResult processCommand(final JsonCommand command) {
final Long transactionId = Long.valueOf(command.getTransactionId()); final Long transactionId = Long.valueOf(command.getTransactionId());
return this.writePlatformService.adjustSavingsTransaction(command.getSavingsId(), transactionId, command); return this.writePlatformService.adjustTransaction(command.getSavingsId(), transactionId, command);
} }
} }
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "DEPOSIT") @CommandType(entity = "CURRENTACCOUNT", action = "DEPOSIT")
public class DepositCurrentAccountCommandHandler implements NewCommandSourceHandler { public class DepositCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService; private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired @Autowired
public DepositCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { public DepositCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService; this.writePlatformService = writePlatformService;
} }
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "HOLDAMOUNT") @CommandType(entity = "CURRENTACCOUNT", action = "HOLDAMOUNT")
public class HoldAmountCurrentAccountCommandHandler implements NewCommandSourceHandler { public class HoldAmountCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService; private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired @Autowired
public HoldAmountCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { public HoldAmountCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService; this.writePlatformService = writePlatformService;
} }
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "RELEASEAMOUNT") @CommandType(entity = "CURRENTACCOUNT", action = "RELEASEAMOUNT")
public class ReleaseAmountCurrentAccountCommandHandler implements NewCommandSourceHandler { public class ReleaseAmountCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService; private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired @Autowired
public ReleaseAmountCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { public ReleaseAmountCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService; this.writePlatformService = writePlatformService;
} }
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "REVERSETRANSACTION") @CommandType(entity = "CURRENTACCOUNT", action = "REVERSETRANSACTION")
public class ReverseTransactionCurrentAccountCommandHandler implements NewCommandSourceHandler { public class ReverseTransactionCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService; private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired @Autowired
public ReverseTransactionCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { public ReverseTransactionCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService; this.writePlatformService = writePlatformService;
} }
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "UNDOTRANSACTION") @CommandType(entity = "CURRENTACCOUNT", action = "UNDOTRANSACTION")
public class UndoTransactionCurrentAccountCommandHandler implements NewCommandSourceHandler { public class UndoTransactionCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService; private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired @Autowired
public UndoTransactionCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { public UndoTransactionCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService; this.writePlatformService = writePlatformService;
} }
@@ -22,7 +22,7 @@ import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler; import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.savings.service.SavingsAccountWritePlatformService; import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -31,10 +31,10 @@ import org.springframework.transaction.annotation.Transactional;
@CommandType(entity = "CURRENTACCOUNT", action = "WITHDRAWAL") @CommandType(entity = "CURRENTACCOUNT", action = "WITHDRAWAL")
public class WithdrawCurrentAccountCommandHandler implements NewCommandSourceHandler { public class WithdrawCurrentAccountCommandHandler implements NewCommandSourceHandler {
private final SavingsAccountWritePlatformService writePlatformService; private final CurrentAccountTransactionWritePlatformService writePlatformService;
@Autowired @Autowired
public WithdrawCurrentAccountCommandHandler(final SavingsAccountWritePlatformService writePlatformService) { public WithdrawCurrentAccountCommandHandler(final CurrentAccountTransactionWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService; this.writePlatformService = writePlatformService;
} }
@@ -27,6 +27,7 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotFoundException; import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotFoundException;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountReadPlatformService; import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountReadPlatformService;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
@@ -226,6 +227,45 @@ public class CurrentAccountReadPlatformServiceImpl implements CurrentAccountRead
return result; return result;
} }
@Override
public List<Map<String, Object>> retrieveAll(final Integer offset, final Integer limit, final String accountNo,
final String clientName) {
final StringBuilder sqlBuilder = new StringBuilder(BASE_SQL);
final List<Object> params = new ArrayList<>();
boolean hasWhere = false;
if (StringUtils.isNotBlank(accountNo)) {
sqlBuilder.append(hasWhere ? " AND " : " WHERE ");
sqlBuilder.append("ca.account_no LIKE ?");
params.add("%" + accountNo + "%");
hasWhere = true;
}
if (StringUtils.isNotBlank(clientName)) {
sqlBuilder.append(hasWhere ? " AND " : " WHERE ");
sqlBuilder.append("c.display_name LIKE ?");
params.add("%" + clientName + "%");
hasWhere = true;
}
if (limit != null && limit > 0) {
sqlBuilder.append(" LIMIT ?");
params.add(limit);
if (offset != null && offset >= 0) {
sqlBuilder.append(" OFFSET ?");
params.add(offset);
}
}
List<Map<String, Object>> rows = jdbcTemplate.query(sqlBuilder.toString(), new FlatRowMapper(), params.toArray());
List<Map<String, Object>> result = new ArrayList<>();
for (Map<String, Object> row : rows) {
final Map<String, Object> response = buildStructuredResponse(row);
final Long id = ((Number) row.get("id")).longValue();
response.put("charges", retrieveCharges(id));
result.add(response);
}
return result;
}
private Map<String, Object> buildStructuredResponse(Map<String, Object> row) { private Map<String, Object> buildStructuredResponse(Map<String, Object> row) {
Map<String, Object> out = new LinkedHashMap<>(); Map<String, Object> out = new LinkedHashMap<>();
@@ -0,0 +1,468 @@
/**
* 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.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMappingRepository;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.PortfolioProductType;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccount;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountRepository;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransaction;
import org.apache.fineract.portfolio.currentaccount.domain.CurrentAccountTransactionRepository;
import org.apache.fineract.portfolio.currentaccount.exception.CurrentAccountNotFoundException;
import org.apache.fineract.portfolio.currentaccount.service.CurrentAccountTransactionWritePlatformService;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProduct;
import org.apache.fineract.portfolio.currentproduct.domain.CurrentProductRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@RequiredArgsConstructor
public class CurrentAccountTransactionWritePlatformServiceImpl implements CurrentAccountTransactionWritePlatformService {
// Savings entity type used for GL journal entries (EntityType.SAVINGS = 2)
private static final Integer ENTITY_TYPE_SAVINGS = 2;
private final PlatformSecurityContext context;
private final CurrentAccountRepository currentAccountRepository;
private final CurrentAccountTransactionRepository currentAccountTransactionRepository;
private final CurrentProductRepository currentProductRepository;
private final ProductToGLAccountMappingRepository productToGLAccountMappingRepository;
private final JournalEntryRepository journalEntryRepository;
@Transactional
@Override
public CommandProcessingResult deposit(final Long accountId, final JsonCommand command) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final Office office = account.getOffice();
LocalDate txnDate = command.localDateValueOfParameterNamed("transactionDate");
if (txnDate == null) txnDate = LocalDate.now();
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
validatePositiveAmount(amount, accountId, "deposit");
final String refNo = command.stringValueOfParameterNamed("accountNumber");
final CurrentAccountTransaction txn = CurrentAccountTransaction.deposit(account, office, amount, txnDate, refNo);
final BigDecimal oldBalance = safeBalance(account.getAccountBalanceDerived());
final BigDecimal newBalance = oldBalance.add(amount);
txn.setRunningBalanceDerived(newBalance);
currentAccountTransactionRepository.saveAndFlush(txn);
account.setAccountBalanceDerived(newBalance);
account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount));
currentAccountRepository.save(account);
postTransactionGl(account, txn, JournalEntryType.DEBIT, JournalEntryType.CREDIT, "Current Account Deposit");
return new CommandProcessingResultBuilder()
.withEntityId(txn.getId())
.withSavingsId(accountId)
.withClientId(account.getClient() != null ? account.getClient().getId() : null)
.withOfficeId(office != null ? office.getId() : null)
.build();
}
@Transactional
@Override
public CommandProcessingResult withdrawal(final Long accountId, final JsonCommand command) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final Office office = account.getOffice();
LocalDate txnDate = command.localDateValueOfParameterNamed("transactionDate");
if (txnDate == null) txnDate = LocalDate.now();
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
validatePositiveAmount(amount, accountId, "withdrawal");
final BigDecimal currentBalance = safeBalance(account.getAccountBalanceDerived());
final BigDecimal onHold = safeBalance(account.getOnHoldFundsDerived());
final BigDecimal available = currentBalance.subtract(onHold);
if (!account.isAllowOverdraft() && amount.compareTo(available) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.insufficient.balance",
"Insufficient available balance. Available: " + available + ", Requested: " + amount, accountId);
}
final String refNo = command.stringValueOfParameterNamed("accountNumber");
final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, office, amount, txnDate, refNo);
final BigDecimal newBalance = currentBalance.subtract(amount);
txn.setRunningBalanceDerived(newBalance);
currentAccountTransactionRepository.saveAndFlush(txn);
account.setAccountBalanceDerived(newBalance);
account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount));
currentAccountRepository.save(account);
// Withdrawal GL: DR SAVINGS_CONTROL (liability down), CR SAVINGS_REFERENCE (cash out)
postTransactionGl(account, txn, JournalEntryType.CREDIT, JournalEntryType.DEBIT, "Current Account Withdrawal");
return new CommandProcessingResultBuilder()
.withEntityId(txn.getId())
.withSavingsId(accountId)
.withClientId(account.getClient() != null ? account.getClient().getId() : null)
.withOfficeId(office != null ? office.getId() : null)
.build();
}
@Transactional
@Override
public CommandProcessingResult holdAmount(final Long accountId, final JsonCommand command) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final Office office = account.getOffice();
LocalDate txnDate = command.localDateValueOfParameterNamed("transactionDate");
if (txnDate == null) txnDate = LocalDate.now();
final BigDecimal amount = command.bigDecimalValueOfParameterNamed("transactionAmount");
validatePositiveAmount(amount, accountId, "hold amount");
final BigDecimal currentBalance = safeBalance(account.getAccountBalanceDerived());
final BigDecimal onHold = safeBalance(account.getOnHoldFundsDerived());
final BigDecimal available = currentBalance.subtract(onHold);
if (amount.compareTo(available) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.insufficient.balance.for.hold",
"Insufficient available balance to place hold. Available: " + available + ", Requested: " + amount, accountId);
}
final String reason = command.stringValueOfParameterNamed("reasonForBlock");
final CurrentAccountTransaction txn = CurrentAccountTransaction.holdAmount(account, office, amount, txnDate, reason);
txn.setRunningBalanceDerived(currentBalance);
currentAccountTransactionRepository.saveAndFlush(txn);
account.setOnHoldFundsDerived(onHold.add(amount));
currentAccountRepository.save(account);
return new CommandProcessingResultBuilder()
.withEntityId(txn.getId())
.withSavingsId(accountId)
.withClientId(account.getClient() != null ? account.getClient().getId() : null)
.withOfficeId(office != null ? office.getId() : null)
.build();
}
@Transactional
@Override
public CommandProcessingResult releaseAmount(final Long accountId, final Long transactionId) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final Office office = account.getOffice();
final CurrentAccountTransaction holdTxn = currentAccountTransactionRepository.findById(transactionId)
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.current.account.transaction.not.found",
"Hold transaction not found: " + transactionId, transactionId));
if (!holdTxn.getCurrentAccount().getId().equals(accountId)) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.not.found",
"Transaction does not belong to this account.", transactionId);
}
if (holdTxn.isReversed()) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.already.reversed",
"Hold transaction is already released/reversed.", transactionId);
}
final BigDecimal holdAmount = holdTxn.getAmount();
final BigDecimal currentBalance = safeBalance(account.getAccountBalanceDerived());
// Mark hold transaction as reversed
holdTxn.setReversed(true);
currentAccountTransactionRepository.save(holdTxn);
// Create release transaction
final CurrentAccountTransaction releaseTxn = CurrentAccountTransaction.releaseAmount(
account, office, holdAmount, LocalDate.now(), transactionId);
releaseTxn.setRunningBalanceDerived(currentBalance);
currentAccountTransactionRepository.saveAndFlush(releaseTxn);
// Reduce onHold
final BigDecimal onHold = safeBalance(account.getOnHoldFundsDerived());
account.setOnHoldFundsDerived(onHold.subtract(holdAmount).max(BigDecimal.ZERO));
currentAccountRepository.save(account);
return new CommandProcessingResultBuilder()
.withEntityId(releaseTxn.getId())
.withSavingsId(accountId)
.build();
}
@Transactional
@Override
public CommandProcessingResult undoTransaction(final Long accountId, final Long transactionId, final boolean isReversal) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final CurrentAccountTransaction txn = currentAccountTransactionRepository.findById(transactionId)
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.current.account.transaction.not.found",
"Transaction not found: " + transactionId, transactionId));
if (!txn.getCurrentAccount().getId().equals(accountId)) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.not.found",
"Transaction does not belong to this account.", transactionId);
}
if (txn.isReversed()) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.already.reversed",
"Transaction is already reversed.", transactionId);
}
reverseTransactionBalances(account, txn);
txn.setReversed(true);
currentAccountTransactionRepository.save(txn);
currentAccountRepository.save(account);
return new CommandProcessingResultBuilder()
.withEntityId(transactionId)
.withSavingsId(accountId)
.build();
}
@Transactional
@Override
public CommandProcessingResult reverseTransaction(final Long accountId, final Long transactionId,
final boolean isReversal, final JsonCommand command) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final CurrentAccountTransaction original = currentAccountTransactionRepository.findById(transactionId)
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.current.account.transaction.not.found",
"Transaction not found: " + transactionId, transactionId));
if (!original.getCurrentAccount().getId().equals(accountId)) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.not.found",
"Transaction does not belong to this account.", transactionId);
}
if (original.isReversed()) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.already.reversed",
"Transaction is already reversed.", transactionId);
}
reverseTransactionBalances(account, original);
original.setReversed(true);
currentAccountTransactionRepository.save(original);
// Create a reversal entry
final CurrentAccountTransaction reversalTxn = CurrentAccountTransaction.reversal(
account, account.getOffice(), original.getAmount(), LocalDate.now(), transactionId);
reversalTxn.setRunningBalanceDerived(safeBalance(account.getAccountBalanceDerived()));
currentAccountTransactionRepository.saveAndFlush(reversalTxn);
currentAccountRepository.save(account);
return new CommandProcessingResultBuilder()
.withEntityId(reversalTxn.getId())
.withSavingsId(accountId)
.build();
}
@Transactional
@Override
public CommandProcessingResult adjustTransaction(final Long accountId, final Long transactionId,
final JsonCommand command) {
context.authenticatedUser();
final CurrentAccount account = findActiveAccount(accountId);
final Office office = account.getOffice();
final CurrentAccountTransaction original = currentAccountTransactionRepository.findById(transactionId)
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.current.account.transaction.not.found",
"Transaction not found: " + transactionId, transactionId));
if (!original.getCurrentAccount().getId().equals(accountId)) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.not.found",
"Transaction does not belong to this account.", transactionId);
}
if (original.isReversed()) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.transaction.already.reversed",
"Transaction is already reversed.", transactionId);
}
final BigDecimal newAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
validatePositiveAmount(newAmount, accountId, "adjust");
LocalDate newDate = command.localDateValueOfParameterNamed("transactionDate");
if (newDate == null) newDate = original.getTransactionDate();
// Undo original
reverseTransactionBalances(account, original);
original.setReversed(true);
currentAccountTransactionRepository.save(original);
// Create adjusted transaction of the same type
final CurrentAccountTransaction adjusted;
if (original.getTransactionTypeEnum() == 1) {
adjusted = CurrentAccountTransaction.deposit(account, office, newAmount, newDate,
command.stringValueOfParameterNamed("accountNumber"));
} else {
adjusted = CurrentAccountTransaction.withdrawal(account, office, newAmount, newDate,
command.stringValueOfParameterNamed("accountNumber"));
}
adjusted.setOriginalTransactionId(transactionId);
adjusted.setRunningBalanceDerived(safeBalance(account.getAccountBalanceDerived()));
currentAccountTransactionRepository.saveAndFlush(adjusted);
// Apply new transaction to balance
applyTransactionToBalance(account, adjusted);
currentAccountRepository.save(account);
return new CommandProcessingResultBuilder()
.withEntityId(adjusted.getId())
.withSavingsId(accountId)
.build();
}
// ── Helpers ─────────────────────────────────────────────────────────────────
private CurrentAccount findActiveAccount(final Long accountId) {
final CurrentAccount account = currentAccountRepository.findById(accountId)
.orElseThrow(() -> new CurrentAccountNotFoundException(accountId));
if (!Integer.valueOf(300).equals(account.getStatusEnum())) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.not.active",
"Current account is not in Active state.", accountId);
}
return account;
}
private void validatePositiveAmount(final BigDecimal amount, final Long accountId, final String operation) {
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new GeneralPlatformDomainRuleException(
"error.msg.current.account." + operation + ".invalid.amount",
"Transaction amount must be greater than zero for " + operation + ".", accountId);
}
}
private BigDecimal safeBalance(final BigDecimal value) {
return value != null ? value : BigDecimal.ZERO;
}
private void reverseTransactionBalances(final CurrentAccount account, final CurrentAccountTransaction txn) {
final BigDecimal amount = txn.getAmount();
final Integer type = txn.getTransactionTypeEnum();
if (Integer.valueOf(1).equals(type)) {
// Was a deposit: reverse it
account.setAccountBalanceDerived(safeBalance(account.getAccountBalanceDerived()).subtract(amount));
account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).subtract(amount).max(BigDecimal.ZERO));
} else if (Integer.valueOf(2).equals(type)) {
// Was a withdrawal: reverse it
account.setAccountBalanceDerived(safeBalance(account.getAccountBalanceDerived()).add(amount));
account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).subtract(amount).max(BigDecimal.ZERO));
}
}
private void applyTransactionToBalance(final CurrentAccount account, final CurrentAccountTransaction txn) {
final BigDecimal amount = txn.getAmount();
final Integer type = txn.getTransactionTypeEnum();
if (Integer.valueOf(1).equals(type)) {
account.setAccountBalanceDerived(safeBalance(account.getAccountBalanceDerived()).add(amount));
account.setTotalDepositsDerived(safeBalance(account.getTotalDepositsDerived()).add(amount));
} else if (Integer.valueOf(2).equals(type)) {
account.setAccountBalanceDerived(safeBalance(account.getAccountBalanceDerived()).subtract(amount));
account.setTotalWithdrawalsDerived(safeBalance(account.getTotalWithdrawalsDerived()).add(amount));
}
}
// GL: cashEntryType=DEBIT/CREDIT for SAVINGS_REFERENCE; controlEntryType for SAVINGS_CONTROL
private void postTransactionGl(final CurrentAccount account, final CurrentAccountTransaction txn,
final JournalEntryType cashEntryType, final JournalEntryType controlEntryType, final String description) {
try {
final CurrentProduct product = currentProductRepository.findById(account.getProductId()).orElse(null);
if (product == null) return;
final Integer accountingType = product.getAccountingType();
if (accountingType == null || accountingType == 1) return; // NONE = 1
final GLAccount cashAccount = findGlAccount(account.getProductId(),
CashAccountsForSavings.SAVINGS_REFERENCE.getValue());
final GLAccount controlAccount = findGlAccount(account.getProductId(),
CashAccountsForSavings.SAVINGS_CONTROL.getValue());
if (cashAccount == null || controlAccount == null) {
log.warn("[GL] Missing GL account mapping for current account product id={}", account.getProductId());
return;
}
final String ref = "CAT" + txn.getId();
postGlEntry(account, cashAccount, cashEntryType, txn.getAmount(), txn.getTransactionDate(), ref, description);
postGlEntry(account, controlAccount, controlEntryType, txn.getAmount(), txn.getTransactionDate(), ref, description);
} catch (Exception e) {
log.error("[GL] Failed GL posting for CA transaction id={}: {}", txn.getId(), e.getMessage(), e);
}
}
private GLAccount findGlAccount(final Long productId, final int financialAccountType) {
final var mapping = productToGLAccountMappingRepository
.findByProductIdAndProductTypeAndFinancialAccountTypeAndPaymentTypeId(
productId, PortfolioProductType.SAVING.getValue(), financialAccountType, null);
return mapping != null ? mapping.getGlAccount() : null;
}
private void postGlEntry(final CurrentAccount account, final GLAccount glAccount,
final JournalEntryType entryType, final BigDecimal amount,
final LocalDate date, final String transactionRef, final String description) {
final JournalEntry entry = JournalEntry.createNew(
account.getOffice(), null, glAccount,
account.getCurrencyCode(), transactionRef,
false, date, entryType, amount,
description,
ENTITY_TYPE_SAVINGS, account.getId(),
null, null, null, null, null);
journalEntryRepository.save(entry);
}
}
@@ -33,6 +33,9 @@ public interface DepositAccountReadPlatformService {
Page<DepositAccountData> retrieveAllPaged(DepositAccountType depositAccountType, PaginationParameters paginationParameters); Page<DepositAccountData> retrieveAllPaged(DepositAccountType depositAccountType, PaginationParameters paginationParameters);
Page<DepositAccountData> retrieveAllPaged(DepositAccountType depositAccountType, PaginationParameters paginationParameters,
String accountNo, String clientName);
Collection<DepositAccountData> retrieveAllForLookup(DepositAccountType depositAccountType); Collection<DepositAccountData> retrieveAllForLookup(DepositAccountType depositAccountType);
DepositAccountData retrieveOne(DepositAccountType depositAccountType, Long accountId); DepositAccountData retrieveOne(DepositAccountType depositAccountType, Long accountId);
@@ -29,9 +29,11 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.data.CodeValueData;
import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService;
import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.infrastructure.core.data.EnumOptionData;
@@ -182,6 +184,38 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
depositAccountMapper); depositAccountMapper);
} }
@Override
public Page<DepositAccountData> retrieveAllPaged(final DepositAccountType depositAccountType,
final PaginationParameters paginationParameters, final String accountNo, final String clientName) {
this.paginationParametersDataValidator.validateParameterValues(paginationParameters, supportedOrderByValues,
depositAccountType.resourceName());
final DepositAccountMapper depositAccountMapper = this.getDepositAccountMapper(depositAccountType);
if (depositAccountMapper == null) {
return null;
}
final StringBuilder sqlBuilder = new StringBuilder(400);
sqlBuilder.append("select " + sqlGenerator.calcFoundRows() + " ");
sqlBuilder.append(depositAccountMapper.schema());
sqlBuilder.append(" where sa.deposit_type_enum = ? ");
final List<Object> params = new ArrayList<>();
params.add(depositAccountType.getValue());
if (StringUtils.isNotBlank(accountNo)) {
sqlBuilder.append(" and sa.account_no like ? ");
params.add("%" + accountNo + "%");
}
if (StringUtils.isNotBlank(clientName)) {
sqlBuilder.append(" and c.display_name like ? ");
params.add("%" + clientName + "%");
}
sqlBuilder.append(paginationParameters.paginationSql());
return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), params.toArray(), depositAccountMapper);
}
@Override @Override
public Collection<DepositAccountData> retrieveAllForLookup(final DepositAccountType depositAccountType) { public Collection<DepositAccountData> retrieveAllForLookup(final DepositAccountType depositAccountType) {
@@ -166,7 +166,7 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
sqlBuilder.append(" join m_office o on o.id = c.office_id"); sqlBuilder.append(" join m_office o on o.id = c.office_id");
sqlBuilder.append(" where o.hierarchy like ?"); sqlBuilder.append(" where o.hierarchy like ?");
final Object[] objectArray = new Object[3]; final Object[] objectArray = new Object[10];
objectArray[0] = hierarchySearchString; objectArray[0] = hierarchySearchString;
int arrayPos = 1; int arrayPos = 1;
if (searchParameters != null) { if (searchParameters != null) {
@@ -186,6 +186,18 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
sqlBuilder.append(" and c.office_id = ?"); sqlBuilder.append(" and c.office_id = ?");
objectArray[arrayPos++] = searchParameters.getOfficeId(); objectArray[arrayPos++] = searchParameters.getOfficeId();
} }
if (StringUtils.isNotBlank(searchParameters.getAccountNo())) {
sqlBuilder.append(" and sa.account_no like ?");
objectArray[arrayPos++] = "%" + searchParameters.getAccountNo() + "%";
}
if (StringUtils.isNotBlank(searchParameters.getName())) {
sqlBuilder.append(" and c.display_name like ?");
objectArray[arrayPos++] = "%" + searchParameters.getName() + "%";
}
if (searchParameters.getDepositType() != null) {
sqlBuilder.append(" and sa.deposit_type_enum = ?");
objectArray[arrayPos++] = searchParameters.getDepositType();
}
if (searchParameters.hasOrderBy()) { if (searchParameters.hasOrderBy()) {
sqlBuilder.append(" order by ").append(searchParameters.getOrderBy()); sqlBuilder.append(" order by ").append(searchParameters.getOrderBy());
this.columnValidator.validateSqlInjection(sqlBuilder.toString(), searchParameters.getOrderBy()); this.columnValidator.validateSqlInjection(sqlBuilder.toString(), searchParameters.getOrderBy());