Merge remote-tracking branch 'self-hosted/feature/account-charges' into feature/Backend-DSC-MFA

This commit is contained in:
Aditya
2026-05-21 16:15:28 +05:30
30 changed files with 14663 additions and 13696 deletions
@@ -73,6 +73,7 @@ public interface JournalEntryMapper {
@Mapping(target = "transactionDetails", ignore = true) @Mapping(target = "transactionDetails", ignore = true)
@Mapping(target = "savingTransactionId", ignore = true) @Mapping(target = "savingTransactionId", ignore = true)
@Mapping(target = "externalAssetOwner", ignore = true) @Mapping(target = "externalAssetOwner", ignore = true)
@Mapping(target = "savingsAccountNo", ignore = true)
JournalEntryData map(JournalEntry journalEntry); JournalEntryData map(JournalEntry journalEntry);
@Named("entityType") @Named("entityType")
@@ -59,6 +59,8 @@ public class JournalEntryData {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private Long entityId; private Long entityId;
@SuppressWarnings("unused") @SuppressWarnings("unused")
private String savingsAccountNo;
@SuppressWarnings("unused")
private Long createdByUserId; private Long createdByUserId;
@SuppressWarnings("unused") @SuppressWarnings("unused")
private OffsetDateTime createdDate; private OffsetDateTime createdDate;
@@ -193,7 +195,8 @@ public class JournalEntryData {
final EnumOptionData entityType, final Long entityId, final Long createdByUserId, final OffsetDateTime submittedOnDate, final EnumOptionData entityType, final Long entityId, final Long createdByUserId, final OffsetDateTime submittedOnDate,
final String createdByUserName, final String comments, final Boolean reversed, final String referenceNumber, final String createdByUserName, final String comments, final Boolean reversed, final String referenceNumber,
final BigDecimal officeRunningBalance, final BigDecimal organizationRunningBalance, final Boolean runningBalanceComputed, final BigDecimal officeRunningBalance, final BigDecimal organizationRunningBalance, final Boolean runningBalanceComputed,
final TransactionDetailData transactionDetailData, final CurrencyData currency, final String externalAssetOwner) { final TransactionDetailData transactionDetailData, final CurrencyData currency, final String externalAssetOwner,
final String savingsAccountNo) {
this.id = id; this.id = id;
this.officeId = officeId; this.officeId = officeId;
this.officeName = officeName; this.officeName = officeName;
@@ -221,6 +224,7 @@ public class JournalEntryData {
this.transactionDetails = transactionDetailData; this.transactionDetails = transactionDetailData;
this.currency = currency; this.currency = currency;
this.externalAssetOwner = externalAssetOwner; this.externalAssetOwner = externalAssetOwner;
this.savingsAccountNo = savingsAccountNo;
} }
public static JournalEntryData importInstance(Long officeId, LocalDate transactionDate, String currencyCode, Long paymentTypeId, public static JournalEntryData importInstance(Long officeId, LocalDate transactionDate, String currencyCode, Long paymentTypeId,
@@ -266,7 +270,7 @@ public class JournalEntryData {
return new JournalEntryData(id, officeId, officeName, glAccountName, glAccountId, glAccountCode, glAccountClassification, return new JournalEntryData(id, officeId, officeName, glAccountName, glAccountId, glAccountCode, glAccountClassification,
transactionDate, entryType, amount, transactionId, manualEntry, entityType, entityId, createdByUserId, submittedOnDate, transactionDate, entryType, amount, transactionId, manualEntry, entityType, entityId, createdByUserId, submittedOnDate,
createdByUserName, comments, reversed, referenceNumber, officeRunningBalance, organizationRunningBalance, createdByUserName, comments, reversed, referenceNumber, officeRunningBalance, organizationRunningBalance,
runningBalanceComputed, transactionDetailData, currency, externalAssetOwner); runningBalanceComputed, transactionDetailData, currency, externalAssetOwner, null);
} }
public Integer getRowIndex() { public Integer getRowIndex() {
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.infrastructure.core.service; package org.apache.fineract.infrastructure.core.service;
import java.time.LocalDate;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
@@ -44,6 +45,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;
@@ -57,6 +59,21 @@ public class SearchParameters {
@Getter(AccessLevel.NONE) @Getter(AccessLevel.NONE)
private Boolean isSelfUser; private Boolean isSelfUser;
private Integer legalForm; private Integer legalForm;
private LocalDate fromDate;
private LocalDate toDate;
private LocalDate asOnDate;
public boolean hasFromDate() {
return this.fromDate != null;
}
public boolean hasToDate() {
return this.toDate != null;
}
public boolean hasAsOnDate() {
return this.asOnDate != null;
}
public Integer getLimit() { public Integer getLimit() {
if (limit == null) { if (limit == null) {
@@ -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;
}
} }
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.portfolio.currentaccount.service; package org.apache.fineract.portfolio.currentaccount.service;
import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -26,4 +27,9 @@ 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);
List<Map<String, Object>> retrieveAll(Integer offset, Integer limit, String accountNo, String clientName,
String status, LocalDate fromDate, LocalDate toDate, LocalDate asOnDate);
} }
@@ -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);
}
@@ -35,4 +35,5 @@ public class SavingsDTO {
private boolean cashBasedAccountingEnabled; private boolean cashBasedAccountingEnabled;
private boolean accrualBasedAccountingEnabled; private boolean accrualBasedAccountingEnabled;
private List<SavingsTransactionDTO> newSavingsTransactions; private List<SavingsTransactionDTO> newSavingsTransactions;
private int depositAccountType;
} }
@@ -83,10 +83,30 @@ public class AccountingProcessorHelper {
public static final String LOAN_TRANSACTION_IDENTIFIER = "L"; public static final String LOAN_TRANSACTION_IDENTIFIER = "L";
public static final String SAVINGS_TRANSACTION_IDENTIFIER = "S"; public static final String SAVINGS_TRANSACTION_IDENTIFIER = "S";
public static final String FIXED_DEPOSIT_TRANSACTION_IDENTIFIER = "FD";
public static final String RECURRING_DEPOSIT_TRANSACTION_IDENTIFIER = "RD";
public static final String CLIENT_TRANSACTION_IDENTIFIER = "C"; public static final String CLIENT_TRANSACTION_IDENTIFIER = "C";
public static final String PROVISIONING_TRANSACTION_IDENTIFIER = "P"; public static final String PROVISIONING_TRANSACTION_IDENTIFIER = "P";
public static final String SHARE_TRANSACTION_IDENTIFIER = "SH"; public static final String SHARE_TRANSACTION_IDENTIFIER = "SH";
private static final ThreadLocal<Integer> DEPOSIT_ACCOUNT_TYPE_CONTEXT = ThreadLocal.withInitial(() -> 100);
public static void setDepositAccountTypeContext(int depositAccountType) {
DEPOSIT_ACCOUNT_TYPE_CONTEXT.set(depositAccountType);
}
public static void clearDepositAccountTypeContext() {
DEPOSIT_ACCOUNT_TYPE_CONTEXT.remove();
}
private String getSavingsTransactionIdentifier() {
return switch (DEPOSIT_ACCOUNT_TYPE_CONTEXT.get()) {
case 200 -> FIXED_DEPOSIT_TRANSACTION_IDENTIFIER;
case 300 -> RECURRING_DEPOSIT_TRANSACTION_IDENTIFIER;
default -> SAVINGS_TRANSACTION_IDENTIFIER;
};
}
private final JournalEntryRepository glJournalEntryRepository; private final JournalEntryRepository glJournalEntryRepository;
private final ProductToGLAccountMappingRepository accountMappingRepository; private final ProductToGLAccountMappingRepository accountMappingRepository;
private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository; private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository;
@@ -203,6 +223,9 @@ public class AccountingProcessorHelper {
final Long loanProductId = (Long) accountingBridgeData.get("savingsProductId"); final Long loanProductId = (Long) accountingBridgeData.get("savingsProductId");
final Long officeId = (Long) accountingBridgeData.get("officeId"); final Long officeId = (Long) accountingBridgeData.get("officeId");
final String currencyCode = (String) accountingBridgeData.get("currencyCode"); final String currencyCode = (String) accountingBridgeData.get("currencyCode");
final int depositAccountType = accountingBridgeData.containsKey("depositAccountType")
? (Integer) accountingBridgeData.get("depositAccountType")
: 100;
final List<SavingsTransactionDTO> newSavingsTransactions = new ArrayList<>(); final List<SavingsTransactionDTO> newSavingsTransactions = new ArrayList<>();
boolean isAccountTransfer = (Boolean) accountingBridgeData.get("isAccountTransfer"); boolean isAccountTransfer = (Boolean) accountingBridgeData.get("isAccountTransfer");
@@ -264,7 +287,7 @@ public class AccountingProcessorHelper {
} }
return new SavingsDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, accrualBasedAccountingEnabled, return new SavingsDTO(loanId, loanProductId, officeId, currencyCode, cashBasedAccountingEnabled, accrualBasedAccountingEnabled,
newSavingsTransactions); newSavingsTransactions, depositAccountType);
} }
public SharesDTO populateSharesDtoFromMap(final Map<String, Object> accountingBridgeData, final boolean cashBasedAccountingEnabled, public SharesDTO populateSharesDtoFromMap(final Map<String, Object> accountingBridgeData, final boolean cashBasedAccountingEnabled,
@@ -898,7 +921,7 @@ public class AccountingProcessorHelper {
String modifiedTransactionId = transactionId; String modifiedTransactionId = transactionId;
if (StringUtils.isNumeric(transactionId)) { if (StringUtils.isNumeric(transactionId)) {
savingsAccountTransactionId = Long.parseLong(transactionId); savingsAccountTransactionId = Long.parseLong(transactionId);
modifiedTransactionId = SAVINGS_TRANSACTION_IDENTIFIER + transactionId; modifiedTransactionId = getSavingsTransactionIdentifier() + transactionId;
} }
final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry,
transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.SAVING.getValue(), savingsId, null, null, transactionDate, JournalEntryType.CREDIT, amount, null, PortfolioProductType.SAVING.getValue(), savingsId, null, null,
@@ -964,7 +987,7 @@ public class AccountingProcessorHelper {
String modifiedTransactionId = transactionId; String modifiedTransactionId = transactionId;
if (StringUtils.isNumeric(transactionId)) { if (StringUtils.isNumeric(transactionId)) {
savingsAccountTransactionId = Long.parseLong(transactionId); savingsAccountTransactionId = Long.parseLong(transactionId);
modifiedTransactionId = SAVINGS_TRANSACTION_IDENTIFIER + transactionId; modifiedTransactionId = getSavingsTransactionIdentifier() + transactionId;
} }
final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry, final JournalEntry journalEntry = JournalEntry.createNew(office, null, account, currencyCode, modifiedTransactionId, manualEntry,
transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.SAVING.getValue(), savingsId, null, null, transactionDate, JournalEntryType.DEBIT, amount, null, PortfolioProductType.SAVING.getValue(), savingsId, null, null,
@@ -40,6 +40,8 @@ public class AccrualBasedAccountingProcessorForSavings implements AccountingProc
@Override @Override
public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) { public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) {
AccountingProcessorHelper.setDepositAccountTypeContext(savingsDTO.getDepositAccountType());
try {
final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(savingsDTO.getOfficeId()); final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(savingsDTO.getOfficeId());
final Long savingsProductId = savingsDTO.getSavingsProductId(); final Long savingsProductId = savingsDTO.getSavingsProductId();
final Long savingsId = savingsDTO.getSavingsId(); final Long savingsId = savingsDTO.getSavingsId();
@@ -281,5 +283,8 @@ public class AccrualBasedAccountingProcessorForSavings implements AccountingProc
paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, feePayments); paymentTypeId, savingsId, transactionId, transactionDate, amount, isReversal, feePayments);
} }
} }
} finally {
AccountingProcessorHelper.clearDepositAccountTypeContext();
}
} }
} }
@@ -39,6 +39,8 @@ public class CashBasedAccountingProcessorForSavings implements AccountingProcess
@Override @Override
public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) { public void createJournalEntriesForSavings(final SavingsDTO savingsDTO) {
AccountingProcessorHelper.setDepositAccountTypeContext(savingsDTO.getDepositAccountType());
try {
final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(savingsDTO.getOfficeId()); final GLClosure latestGLClosure = this.helper.getLatestClosureByBranch(savingsDTO.getOfficeId());
final Long savingsProductId = savingsDTO.getSavingsProductId(); final Long savingsProductId = savingsDTO.getSavingsProductId();
final Long savingsId = savingsDTO.getSavingsId(); final Long savingsId = savingsDTO.getSavingsId();
@@ -251,5 +253,8 @@ public class CashBasedAccountingProcessorForSavings implements AccountingProcess
savingsId, transactionId, transactionDate, amount, isReversal, feePayments); savingsId, transactionId, transactionDate, amount, isReversal, feePayments);
} }
} }
} finally {
AccountingProcessorHelper.clearDepositAccountTypeContext();
}
} }
} }
@@ -104,7 +104,12 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
.append(" journalEntry.created_on_utc as submittedOnDate, journalEntry.reversed as reversed, ") .append(" journalEntry.created_on_utc as submittedOnDate, journalEntry.reversed as reversed, ")
.append(" journalEntry.currency_code as currencyCode, curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ") .append(" journalEntry.currency_code as currencyCode, curr.name as currencyName, curr.internationalized_name_code as currencyNameCode, ")
.append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ") .append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ")
.append(" eao.external_id as externalAssetOwner "); .append(" eao.external_id as externalAssetOwner, ")
.append(" CASE ")
.append(" WHEN journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id LIKE 'CA%' THEN ca.account_no ")
.append(" WHEN journalEntry.entity_type_enum = 2 THEN sa.account_no ")
.append(" ELSE NULL ")
.append(" END as savingsAccountNo ");
if (associationParametersData.isRunningBalanceRequired()) { if (associationParametersData.isRunningBalanceRequired()) {
sb.append(" ,journalEntry.is_running_balance_calculated as runningBalanceComputed, ") sb.append(" ,journalEntry.is_running_balance_calculated as runningBalanceComputed, ")
.append(" journalEntry.office_running_balance as officeRunningBalance, ") .append(" journalEntry.office_running_balance as officeRunningBalance, ")
@@ -124,7 +129,9 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
.append(" left join m_appuser as creatingUser on creatingUser.id = journalEntry.created_by ") .append(" left join m_appuser as creatingUser on creatingUser.id = journalEntry.created_by ")
.append(" join m_currency curr on curr.code = journalEntry.currency_code ") .append(" join m_currency curr on curr.code = journalEntry.currency_code ")
.append(" left join m_external_asset_owner_journal_entry_mapping eajem on eajem.journal_entry_id = journalEntry.id ") .append(" left join m_external_asset_owner_journal_entry_mapping eajem on eajem.journal_entry_id = journalEntry.id ")
.append(" left join m_external_asset_owner eao on eao.id = eajem.owner_id "); .append(" left join m_external_asset_owner eao on eao.id = eajem.owner_id ")
.append(" left join m_savings_account sa on sa.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id NOT LIKE 'CA%' ")
.append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 AND journalEntry.transaction_id LIKE 'CA%' ");
if (associationParametersData.isTransactionDetailsRequired()) { if (associationParametersData.isTransactionDetailsRequired()) {
sb.append(" left join m_loan_transaction as lt on journalEntry.loan_transaction_id = lt.id ") sb.append(" left join m_loan_transaction as lt on journalEntry.loan_transaction_id = lt.id ")
.append(" left join m_savings_account_transaction as st on journalEntry.savings_transaction_id = st.id ") .append(" left join m_savings_account_transaction as st on journalEntry.savings_transaction_id = st.id ")
@@ -235,11 +242,12 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
transactionDetailData = new TransactionDetailData(transaction, paymentDetailData, noteData, transactionTypeEnumData); transactionDetailData = new TransactionDetailData(transaction, paymentDetailData, noteData, transactionTypeEnumData);
} }
final String externalAssetOwner = rs.getString("externalAssetOwner"); final String externalAssetOwner = rs.getString("externalAssetOwner");
final String savingsAccountNo = rs.getString("savingsAccountNo");
return new JournalEntryData(id, officeId, officeName, glAccountName, glAccountId, glCode, accountType, transactionDate, return new JournalEntryData(id, officeId, officeName, glAccountName, glAccountId, glCode, accountType, transactionDate,
entryType, amount, transactionId, manualEntry, entityType, entityId, createdByUserId, submittedOnDate, entryType, amount, transactionId, manualEntry, entityType, entityId, createdByUserId, submittedOnDate,
createdByUserName, comments, reversed, referenceNumber, officeRunningBalance, organizationRunningBalance, createdByUserName, comments, reversed, referenceNumber, officeRunningBalance, organizationRunningBalance,
runningBalanceComputed, transactionDetailData, currency, externalAssetOwner); runningBalanceComputed, transactionDetailData, currency, externalAssetOwner, savingsAccountNo);
} }
} }
@@ -278,7 +278,7 @@ public class JournalEntryRunningBalanceUpdateServiceImpl implements JournalEntry
final EnumOptionData entryType = AccountingEnumerations.journalEntryType(entryTypeId); final EnumOptionData entryType = AccountingEnumerations.journalEntryType(entryTypeId);
return new JournalEntryData(id, officeId, null, null, glAccountId, null, accountType, null, entryType, amount, null, null, null, return new JournalEntryData(id, officeId, null, null, glAccountId, null, accountType, null, entryType, amount, null, null, null,
null, null, null, null, null, null, null, null, null, null, null, null, null); null, null, null, null, null, null, null, null, null, null, null, null, null, null);
} }
} }
@@ -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}")
@@ -38,6 +38,7 @@ import jakarta.ws.rs.core.UriInfo;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import java.time.LocalDate;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -101,10 +102,20 @@ 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,
@QueryParam("status") @Parameter(description = "status") final String status,
@QueryParam("fromDate") @Parameter(description = "fromDate (YYYY-MM-DD)") final String fromDate,
@QueryParam("toDate") @Parameter(description = "toDate (YYYY-MM-DD)") final String toDate,
@QueryParam("asOnDate") @Parameter(description = "asOnDate (YYYY-MM-DD)") final String asOnDate) {
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,
status,
fromDate != null ? LocalDate.parse(fromDate) : null,
toDate != null ? LocalDate.parse(toDate) : null,
asOnDate != null ? LocalDate.parse(asOnDate) : null);
return GSON.toJson(accounts); return GSON.toJson(accounts);
} }
@@ -43,6 +43,7 @@ import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import java.io.InputStream; import java.io.InputStream;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
@@ -159,7 +160,13 @@ 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,
@QueryParam("status") @Parameter(description = "status") final String status,
@QueryParam("fromDate") @Parameter(description = "fromDate (YYYY-MM-DD)") final String fromDate,
@QueryParam("toDate") @Parameter(description = "toDate (YYYY-MM-DD)") final String toDate,
@QueryParam("asOnDate") @Parameter(description = "asOnDate (YYYY-MM-DD)") final String asOnDate) {
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 +177,11 @@ 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,
status,
fromDate != null ? LocalDate.parse(fromDate) : null,
toDate != null ? LocalDate.parse(toDate) : null,
asOnDate != null ? LocalDate.parse(asOnDate) : null);
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);
} }
@@ -43,6 +43,7 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import java.io.InputStream; import java.io.InputStream;
import java.time.LocalDate;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
@@ -140,7 +141,13 @@ 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,
@QueryParam("status") @Parameter(description = "status") final String status,
@QueryParam("fromDate") @Parameter(description = "fromDate (YYYY-MM-DD)") final String fromDate,
@QueryParam("toDate") @Parameter(description = "toDate (YYYY-MM-DD)") final String toDate,
@QueryParam("asOnDate") @Parameter(description = "asOnDate (YYYY-MM-DD)") final String asOnDate) {
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 +158,11 @@ 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,
status,
fromDate != null ? LocalDate.parse(fromDate) : null,
toDate != null ? LocalDate.parse(toDate) : null,
asOnDate != null ? LocalDate.parse(asOnDate) : null);
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);
} }
@@ -40,6 +40,7 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import java.io.InputStream; import java.io.InputStream;
import java.time.LocalDate;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Set; import java.util.Set;
@@ -130,7 +131,14 @@ 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,
@QueryParam("status") @Parameter(description = "status") final String status,
@QueryParam("fromDate") @Parameter(description = "fromDate (YYYY-MM-DD)") final String fromDate,
@QueryParam("toDate") @Parameter(description = "toDate (YYYY-MM-DD)") final String toDate,
@QueryParam("asOnDate") @Parameter(description = "asOnDate (YYYY-MM-DD)") final String asOnDate) {
context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME); context.authenticatedUser().validateHasReadPermission(SavingsApiConstants.SAVINGS_ACCOUNT_RESOURCE_NAME);
@@ -138,7 +146,12 @@ 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)
.status(status)
.fromDate(fromDate != null ? LocalDate.parse(fromDate) : null)
.toDate(toDate != null ? LocalDate.parse(toDate) : null)
.asOnDate(asOnDate != null ? LocalDate.parse(asOnDate) : null)
.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;
} }
@@ -22,11 +22,13 @@ import java.math.BigDecimal;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.ResultSetMetaData; import java.sql.ResultSetMetaData;
import java.sql.SQLException; import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; 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 +228,102 @@ 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) {
return retrieveAll(offset, limit, accountNo, clientName, null, null, null, null);
}
@Override
public List<Map<String, Object>> retrieveAll(final Integer offset, final Integer limit, final String accountNo,
final String clientName, final String status, final LocalDate fromDate, final LocalDate toDate,
final LocalDate asOnDate) {
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 (StringUtils.isNotBlank(status)) {
final Integer statusEnum = resolveStatusEnum(status);
if (statusEnum != null) {
sqlBuilder.append(hasWhere ? " AND " : " WHERE ");
sqlBuilder.append("ca.status_enum = ?");
params.add(statusEnum);
hasWhere = true;
}
}
if (fromDate != null) {
sqlBuilder.append(hasWhere ? " AND " : " WHERE ");
sqlBuilder.append("ca.activatedon_date >= ?");
params.add(fromDate);
hasWhere = true;
}
if (toDate != null) {
sqlBuilder.append(hasWhere ? " AND " : " WHERE ");
sqlBuilder.append("ca.activatedon_date <= ?");
params.add(toDate);
hasWhere = true;
}
if (asOnDate != null) {
sqlBuilder.append(hasWhere ? " AND " : " WHERE ");
sqlBuilder.append("ca.activatedon_date <= ?");
params.add(asOnDate);
}
sqlBuilder.append(" ORDER BY ca.id DESC");
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 Integer resolveStatusEnum(final String status) {
if (status == null) {
return null;
}
return switch (status.toLowerCase()) {
case "active" -> 300;
case "approved" -> 200;
case "submitted", "submittedandpendingapproval" -> 100;
case "withdrawn" -> 400;
case "rejected" -> 500;
case "closed" -> 600;
default -> {
try {
yield Integer.parseInt(status);
} catch (NumberFormatException e) {
yield null;
}
}
};
}
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);
}
}
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.portfolio.savings.service; package org.apache.fineract.portfolio.savings.service;
import java.time.LocalDate;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import org.apache.fineract.infrastructure.core.data.PaginationParameters; import org.apache.fineract.infrastructure.core.data.PaginationParameters;
@@ -33,6 +34,12 @@ 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);
Page<DepositAccountData> retrieveAllPaged(DepositAccountType depositAccountType, PaginationParameters paginationParameters,
String accountNo, String clientName, String status, LocalDate fromDate, LocalDate toDate, LocalDate asOnDate);
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,92 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
depositAccountMapper); depositAccountMapper);
} }
@Override
public Page<DepositAccountData> retrieveAllPaged(final DepositAccountType depositAccountType,
final PaginationParameters paginationParameters, final String accountNo, final String clientName) {
return retrieveAllPaged(depositAccountType, paginationParameters, accountNo, clientName, null, null, null, null);
}
@Override
public Page<DepositAccountData> retrieveAllPaged(final DepositAccountType depositAccountType,
final PaginationParameters paginationParameters, final String accountNo, final String clientName,
final String status, final LocalDate fromDate, final LocalDate toDate, final LocalDate asOnDate) {
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 + "%");
}
if (StringUtils.isNotBlank(status)) {
final Integer statusEnum = resolveStatusEnum(status);
if (statusEnum != null) {
sqlBuilder.append(" and sa.status_enum = ? ");
params.add(statusEnum);
}
}
if (fromDate != null) {
sqlBuilder.append(" and sa.activatedon_date >= ? ");
params.add(fromDate);
}
if (toDate != null) {
sqlBuilder.append(" and sa.activatedon_date <= ? ");
params.add(toDate);
}
if (asOnDate != null) {
sqlBuilder.append(" and sa.activatedon_date <= ? ");
params.add(asOnDate);
}
if (paginationParameters.hasOrderBy()) {
sqlBuilder.append(paginationParameters.paginationSql());
} else {
sqlBuilder.append(" order by sa.id desc ");
sqlBuilder.append(paginationParameters.limitSql());
}
return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), params.toArray(), depositAccountMapper);
}
private Integer resolveStatusEnum(final String status) {
if (status == null) {
return null;
}
return switch (status.toLowerCase()) {
case "active" -> 300;
case "approved" -> 200;
case "submitted", "submittedandpendingapproval" -> 100;
case "withdrawn" -> 400;
case "rejected" -> 500;
case "closed" -> 600;
default -> {
try {
yield Integer.parseInt(status);
} catch (NumberFormatException e) {
yield null;
}
}
};
}
@Override @Override
public Collection<DepositAccountData> retrieveAllForLookup(final DepositAccountType depositAccountType) { public Collection<DepositAccountData> retrieveAllForLookup(final DepositAccountType depositAccountType) {
@@ -166,15 +166,17 @@ 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[15];
objectArray[0] = hierarchySearchString; objectArray[0] = hierarchySearchString;
int arrayPos = 1; int arrayPos = 1;
if (searchParameters != null) { if (searchParameters != null) {
if (StringUtils.isNotBlank(searchParameters.getStatus())) { if (StringUtils.isNotBlank(searchParameters.getStatus())) {
sqlBuilder.append(" and sa.status_enum = ?"); final Integer statusEnum = resolveStatusEnum(searchParameters.getStatus());
objectArray[arrayPos] = Integer.parseInt(searchParameters.getStatus()); if (statusEnum != null) {
arrayPos = arrayPos + 1; sqlBuilder.append(" and sa.status_enum = ?");
objectArray[arrayPos++] = statusEnum;
}
} }
if (StringUtils.isNotBlank(searchParameters.getExternalId())) { if (StringUtils.isNotBlank(searchParameters.getExternalId())) {
@@ -186,6 +188,30 @@ 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.hasFromDate()) {
sqlBuilder.append(" and sa.activatedon_date >= ?");
objectArray[arrayPos++] = searchParameters.getFromDate();
}
if (searchParameters.hasToDate()) {
sqlBuilder.append(" and sa.activatedon_date <= ?");
objectArray[arrayPos++] = searchParameters.getToDate();
}
if (searchParameters.hasAsOnDate()) {
sqlBuilder.append(" and sa.activatedon_date <= ?");
objectArray[arrayPos++] = searchParameters.getAsOnDate();
}
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());
@@ -194,6 +220,8 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
sqlBuilder.append(' ').append(searchParameters.getSortOrder()); sqlBuilder.append(' ').append(searchParameters.getSortOrder());
this.columnValidator.validateSqlInjection(sqlBuilder.toString(), searchParameters.getSortOrder()); this.columnValidator.validateSqlInjection(sqlBuilder.toString(), searchParameters.getSortOrder());
} }
} else {
sqlBuilder.append(" order by sa.id desc");
} }
if (searchParameters.hasLimit()) { if (searchParameters.hasLimit()) {
@@ -209,6 +237,27 @@ public class SavingsAccountReadPlatformServiceImpl implements SavingsAccountRead
return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), finalObjectArray, this.savingAccountMapper); return this.paginationHelper.fetchPage(this.jdbcTemplate, sqlBuilder.toString(), finalObjectArray, this.savingAccountMapper);
} }
private Integer resolveStatusEnum(final String status) {
if (status == null) {
return null;
}
return switch (status.toLowerCase()) {
case "active" -> 300;
case "approved" -> 200;
case "submitted", "submittedandpendingapproval" -> 100;
case "withdrawn" -> 400;
case "rejected" -> 500;
case "closed" -> 600;
default -> {
try {
yield Integer.parseInt(status);
} catch (NumberFormatException e) {
yield null;
}
}
};
}
@Override @Override
public SavingsAccountData retrieveOne(final Long accountId) { public SavingsAccountData retrieveOne(final Long accountId) {
@@ -1894,6 +1894,7 @@ public class SavingsAccount extends AbstractAuditableWithUTCDateTimeCustom<Long>
accountingBridgeData.put("cashBasedAccountingEnabled", isCashBasedAccountingEnabledOnSavingsProduct()); accountingBridgeData.put("cashBasedAccountingEnabled", isCashBasedAccountingEnabledOnSavingsProduct());
accountingBridgeData.put("accrualBasedAccountingEnabled", isAccrualBasedAccountingEnabledOnSavingsProduct()); accountingBridgeData.put("accrualBasedAccountingEnabled", isAccrualBasedAccountingEnabledOnSavingsProduct());
accountingBridgeData.put("isAccountTransfer", isAccountTransfer); accountingBridgeData.put("isAccountTransfer", isAccountTransfer);
accountingBridgeData.put("depositAccountType", depositAccountType().getValue());
final List<Map<String, Object>> newSavingsTransactions = new ArrayList<>(); final List<Map<String, Object>> newSavingsTransactions = new ArrayList<>();