feat: fix standing instructions GL accounting for current account transfers

- AccountTransfersWritePlatformServiceImpl:
  * Direct GL posting for current↔savings/current transfers:
    DR debit-side account GL (with correct account entity_id)
    CR credit-side account GL (with correct account entity_id)
  * Prefix current account txn_id with CA (not C) so
    JournalEntryReadPlatformServiceImpl joins m_current_account
    and shows correct current account number on GL entries
  * Suppress redundant savings deposit/withdrawal GL entries
    (skipAccountingPostings=true) when current account handles
    the full double-entry directly
  * Separate performCurrentWithdrawalForLoan for Current→Loan
    transfers (loan domain service handles the other GL leg)
  * Reuse existing AccountTransferDetails for SI recurring runs
    so transactions appear in list-account-transactions history

- SavingsAccountDomainService / SavingsAccountDomainServiceJpa:
  * New handleDeposit overload with skipAccountingPostings flag
  * handleWithdrawal respects SavingsTransactionBooleanValues
    .isSkipAccountingPostings() to suppress redundant GL entries

- SavingsTransactionBooleanValues:
  * Added skipAccountingPostings field + overloaded constructor

- AccountingProcessorHelper:
  * New createDirectTransferJournalEntriesForSavings() method
    creates DR and CR entries with separate productId/entityId
    so each GL line shows the correct account number

- JournalEntryReadPlatformServiceImpl:
  * Join both m_savings_account and m_current_account always
  * COALESCE(ca.account_no, sa.account_no) to resolve correct
    account number regardless of transaction_id prefix

- ExecuteStandingInstructionsTasklet:
  * Load existing AccountTransferDetails via repository so
    subsequent SI runs link to the same transfer detail record

- ExecuteStandingInstructionsConfig / AccountConfiguration:
  * Wire AccountTransferDetailRepository and
    AccountingProcessorHelper into the relevant beans

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-05-26 16:30:04 +05:30
co-authored by Claude Sonnet 4.6
parent 2251fd2259
commit 7d4f409202
9 changed files with 738 additions and 555 deletions
@@ -600,6 +600,27 @@ public class AccountingProcessorHelper {
createCreditJournalEntryForSavings(office, currencyCode, creditAccount, savingsId, transactionId, transactionDate, amount);
}
/**
* Creates a direct transfer journal entry between two savings-type accounts (savings or current),
* each with their own product GL mapping and entity ID so the correct account number appears per entry.
*
* @param debitProductId product ID whose SAVINGS_CONTROL GL is debited
* @param debitEntityId entity (account) ID shown on the debit entry
* @param creditProductId product ID whose SAVINGS_CONTROL GL is credited
* @param creditEntityId entity (account) ID shown on the credit entry
*/
public void createDirectTransferJournalEntriesForSavings(final Office office, final String currencyCode,
final Long debitProductId, final Long debitEntityId,
final Long creditProductId, final Long creditEntityId,
final String transactionId, final LocalDate transactionDate, final BigDecimal amount) {
final GLAccount debitAccount = getLinkedGLAccountForSavingsProduct(debitProductId,
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), null);
final GLAccount creditAccount = getLinkedGLAccountForSavingsProduct(creditProductId,
CashAccountsForSavings.SAVINGS_CONTROL.getValue(), null);
createDebitJournalEntryForSavings(office, currencyCode, debitAccount, debitEntityId, transactionId, transactionDate, amount);
createCreditJournalEntryForSavings(office, currencyCode, creditAccount, creditEntityId, transactionId, transactionDate, amount);
}
/**
* Convenience method that creates a pair of related Debits and Credits for Cash Based accounting.
*
@@ -106,8 +106,7 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
.append(" curr.display_symbol as currencyDisplaySymbol, curr.decimal_places as currencyDigits, curr.currency_multiplesof as inMultiplesOf, ")
.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(" WHEN journalEntry.entity_type_enum = 2 THEN COALESCE(ca.account_no, sa.account_no) ")
.append(" ELSE NULL ")
.append(" END as savingsAccountNo ");
if (associationParametersData.isRunningBalanceRequired()) {
@@ -130,8 +129,8 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
.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 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%' ");
.append(" left join m_savings_account sa on sa.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ")
.append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ");
if (associationParametersData.isTransactionDetailsRequired()) {
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 ")
@@ -1,70 +1,73 @@
/**
* 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.account.jobs.executestandinginstructions;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class ExecuteStandingInstructionsConfig {
@Autowired
private JobRepository jobRepository;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private StandingInstructionReadPlatformService standingInstructionReadPlatformService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private DatabaseSpecificSQLGenerator sqlGenerator;
@Autowired
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Bean
protected Step executeStandingInstructionsStep() {
return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository)
.tasklet(executeStandingInstructionsTasklet(), transactionManager).build();
}
@Bean
public Job executeStandingInstructionsJob() {
return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() {
return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator,
accountTransfersWritePlatformService);
}
}
/**
* 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.account.jobs.executestandinginstructions;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class ExecuteStandingInstructionsConfig {
@Autowired
private JobRepository jobRepository;
@Autowired
private PlatformTransactionManager transactionManager;
@Autowired
private StandingInstructionReadPlatformService standingInstructionReadPlatformService;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private DatabaseSpecificSQLGenerator sqlGenerator;
@Autowired
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Autowired
private AccountTransferDetailRepository accountTransferDetailRepository;
@Bean
protected Step executeStandingInstructionsStep() {
return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository)
.tasklet(executeStandingInstructionsTasklet(), transactionManager).build();
}
@Bean
public Job executeStandingInstructionsJob() {
return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep())
.incrementer(new RunIdIncrementer()).build();
}
@Bean
public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() {
return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator,
accountTransfersWritePlatformService, accountTransferDetailRepository);
}
}
@@ -34,6 +34,8 @@ import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException;
import org.apache.fineract.portfolio.account.data.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.domain.AccountTransferDetails;
import org.apache.fineract.portfolio.account.domain.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
@@ -58,6 +60,7 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet {
private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator;
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
private final AccountTransferDetailRepository accountTransferDetailRepository;
@Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
@@ -105,10 +108,15 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet {
final SavingsAccount fromSavingsAccount = null;
final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false;
// Load existing AccountTransferDetails so transactions are linked to
// this standing instruction and appear in its transaction history
final AccountTransferDetails existingDetails = data.getAccountDetailId() != null
? accountTransferDetailRepository.findById(data.getAccountDetailId()).orElse(null)
: null;
AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount,
data.fromAccountTypeEnum(), data.toAccountTypeEnum(), data.getFromAccount().getId(), data.getToAccount().getId(),
data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null,
data.transferTypeEnum().getValue(), null, null, ExternalId.empty(), null, null, fromSavingsAccount,
data.transferTypeEnum().getValue(), existingDetails, null, ExternalId.empty(), null, null, fromSavingsAccount,
isRegularTransaction, isExceptionForBalanceCheck);
final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId());
@@ -34,6 +34,9 @@ import java.util.Collection;
import java.util.List;
import java.util.Locale;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings;
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
import org.apache.fineract.accounting.journalentry.service.AccountingProcessorHelper;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
@@ -96,6 +99,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
private final FineractProperties fineractProperties;
private final CurrentAccountRepository currentAccountRepository;
private final CurrentAccountTransactionRepository currentAccountTransactionRepository;
private final AccountingProcessorHelper accountingProcessorHelper;
@Transactional
@Override
@@ -467,23 +471,29 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
.assembleFrom(accountTransferDTO.getToAccountId(), backdatedTxnsAllowedTill);
final CurrentAccountTransaction withdrawal = performCurrentWithdrawal(fromCurrentAccount,
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getTransactionDate());
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getTransactionDate(),
toSavingsAccount.productId(), toSavingsAccount.getId());
// skipAccountingPostings=true: current account withdrawal leg (C-txn) already posts
// DR CURRENT ACCOUNT / CR SAVING INDIVIDUAL directly — each with its own account number.
// Savings deposit GL entries would produce a self-cancelling DR+CR on SAVING INDIVIDUAL — suppress them.
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount,
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(),
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill, true);
final AccountTransferDetails newDetails = AccountTransferDetails.currentToSavingsTransfer(
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
toSavingsAccount.office(), toSavingsAccount.getClient(), toSavingsAccount,
accountTransferDTO.getTransferType());
if (accountTransferDetails == null) {
accountTransferDetails = AccountTransferDetails.currentToSavingsTransfer(
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
toSavingsAccount.office(), toSavingsAccount.getClient(), toSavingsAccount,
accountTransferDTO.getTransferType());
}
final Money amount = Money.of(toSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount());
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
newDetails, null, deposit, accountTransferDTO.getTransactionDate(), amount,
accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
accountTransferDetails, null, deposit, accountTransferDTO.getTransactionDate(), amount,
accountTransferDTO.getDescription()));
this.accountTransferDetailRepository.saveAndFlush(newDetails);
transferTransactionId = newDetails.getId();
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
transferTransactionId = accountTransferDetails.getId();
} else if (isSavingsToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
// --- Savings Account → Current Account ---
@@ -494,27 +504,33 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId()));
// skipAccountingPostings=true: current account deposit leg (C-txn) posts
// DR SAVING INDIVIDUAL / CR CURRENT ACCOUNT directly. Savings withdrawal GL entries
// would produce a self-cancelling DR+CR on the same SAVING INDIVIDUAL GL — suppress them.
final SavingsTransactionBooleanValues txnBooleans = new SavingsTransactionBooleanValues(isAccountTransfer,
isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), false,
accountTransferDTO.isExceptionForBalanceCheck());
accountTransferDTO.isExceptionForBalanceCheck(), true);
final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount,
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(),
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
txnBooleans, backdatedTxnsAllowedTill);
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getTransactionDate());
accountTransferDTO.getTransactionDate(),
fromSavingsAccount.productId(), fromSavingsAccount.getId());
final AccountTransferDetails newDetails = AccountTransferDetails.savingsToCurrentTransfer(
fromSavingsAccount.office(), fromSavingsAccount.getClient(), fromSavingsAccount,
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
accountTransferDTO.getTransferType());
if (accountTransferDetails == null) {
accountTransferDetails = AccountTransferDetails.savingsToCurrentTransfer(
fromSavingsAccount.office(), fromSavingsAccount.getClient(), fromSavingsAccount,
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
accountTransferDTO.getTransferType());
}
final Money amount = Money.of(fromSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount());
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
newDetails, withdrawal, null, accountTransferDTO.getTransactionDate(), amount,
accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
accountTransferDetails, withdrawal, null, accountTransferDTO.getTransactionDate(), amount,
accountTransferDTO.getDescription()));
this.accountTransferDetailRepository.saveAndFlush(newDetails);
transferTransactionId = newDetails.getId();
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
transferTransactionId = accountTransferDetails.getId();
} else if (isCurrentToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
// --- Current Account → Current Account ---
@@ -528,22 +544,26 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId()));
performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getTransactionDate());
accountTransferDTO.getTransactionDate(),
toCurrentAccount.getProductId(), toCurrentAccount.getId());
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getTransactionDate());
accountTransferDTO.getTransactionDate(),
fromCurrentAccount.getProductId(), fromCurrentAccount.getId());
final AccountTransferDetails newDetails = AccountTransferDetails.currentToCurrentTransfer(
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
accountTransferDTO.getTransferType());
if (accountTransferDetails == null) {
accountTransferDetails = AccountTransferDetails.currentToCurrentTransfer(
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
accountTransferDTO.getTransferType());
}
final MonetaryCurrency currency = new MonetaryCurrency(fromCurrentAccount.getCurrencyCode(),
fromCurrentAccount.getCurrencyDigits(), fromCurrentAccount.getCurrencyMultiplesOf());
final Money amount = Money.of(currency, accountTransferDTO.getTransactionAmount());
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
newDetails, null, null, accountTransferDTO.getTransactionDate(), amount,
accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
accountTransferDetails, null, null, accountTransferDTO.getTransactionDate(), amount,
accountTransferDTO.getDescription()));
this.accountTransferDetailRepository.saveAndFlush(newDetails);
transferTransactionId = newDetails.getId();
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
transferTransactionId = accountTransferDetails.getId();
} else if (isCurrentToLoanAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
// --- Current Account → Loan Account ---
@@ -553,7 +573,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId()));
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId());
performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
performCurrentWithdrawalForLoan(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getTransactionDate());
ExternalId externalId = externalIdFactory.create(accountTransferDTO.getTxnExternalId().getValue());
@@ -656,10 +676,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
}
/**
* Withdraw amount from a current account and save the transaction.
* Throws {@link GeneralPlatformDomainRuleException} if balance is insufficient.
* Withdraw for Current → Loan transfers. Loan domain service handles the loan-side GL,
* so current account side uses LIABILITY_TRANSFER as the counterpart (classic approach).
*/
private CurrentAccountTransaction performCurrentWithdrawal(final CurrentAccount account, final BigDecimal amount,
private CurrentAccountTransaction performCurrentWithdrawalForLoan(final CurrentAccount account, final BigDecimal amount,
final LocalDate txnDate) {
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO;
@@ -677,6 +697,40 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
account.setTotalWithdrawalsDerived(
(account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount));
currentAccountRepository.save(account);
if (account.getProductId() != null && txn.getId() != null) {
final String currentTxnId = "CA" + txn.getId();
accountingProcessorHelper.createCashBasedJournalEntriesAndReversalsForSavings(account.getOffice(),
account.getCurrencyCode(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(),
FinancialActivity.LIABILITY_TRANSFER.getValue(), account.getProductId(), null,
account.getId(), currentTxnId, txnDate, amount, false);
}
return txn;
}
/**
* Withdraw amount from a current account and save the transaction.
* Throws {@link GeneralPlatformDomainRuleException} if balance is insufficient.
*/
private CurrentAccountTransaction performCurrentWithdrawal(final CurrentAccount account, final BigDecimal amount,
final LocalDate txnDate, final Long counterpartProductId, final Long counterpartEntityId) {
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO;
final BigDecimal available = balance.subtract(onHold);
if (!account.isAllowOverdraft() && amount.compareTo(available) > 0) {
throw new GeneralPlatformDomainRuleException("error.msg.current.account.insufficient.balance",
"Insufficient balance in current account " + account.getId()
+ ". Available: " + available + ", Requested: " + amount, account.getId());
}
final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, account.getOffice(), amount, txnDate, null);
final BigDecimal newBalance = balance.subtract(amount);
txn.setRunningBalanceDerived(newBalance);
currentAccountTransactionRepository.saveAndFlush(txn);
account.setAccountBalanceDerived(newBalance);
account.setTotalWithdrawalsDerived(
(account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount));
currentAccountRepository.save(account);
// Post journal entries: DR current account GL, CR counterpart GL — each with correct entity/account number
postCurrentAccountTransferJournalEntries(account, txn, amount, txnDate, true, counterpartProductId, counterpartEntityId);
return txn;
}
@@ -684,7 +738,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
* Deposit amount into a current account and save the transaction.
*/
private CurrentAccountTransaction performCurrentDeposit(final CurrentAccount account, final BigDecimal amount,
final LocalDate txnDate) {
final LocalDate txnDate, final Long counterpartProductId, final Long counterpartEntityId) {
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
final CurrentAccountTransaction txn = CurrentAccountTransaction.deposit(account, account.getOffice(), amount, txnDate, null);
final BigDecimal newBalance = balance.add(amount);
@@ -694,9 +748,67 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
account.setTotalDepositsDerived(
(account.getTotalDepositsDerived() != null ? account.getTotalDepositsDerived() : BigDecimal.ZERO).add(amount));
currentAccountRepository.save(account);
// Post journal entries: DR counterpart GL, CR current account GL — each with correct entity/account number
postCurrentAccountTransferJournalEntries(account, txn, amount, txnDate, false, counterpartProductId, counterpartEntityId);
return txn;
}
/**
* Posts double-entry journal entries for a current account leg of an account transfer.
* <p>
* Current Product GL mappings are stored under {@code PortfolioProductType.SAVING}, so the
* savings JE helper resolves them correctly.
* <p>
* Transaction id is prefixed with "C" (e.g. {@code C57}) so the entries are NOT grouped with
* savings entries that share the same numeric id (those use prefix "S"). The helper only
* appends its own "S" prefix when the id is numeric; passing "C57" keeps it as-is.
*
* @param isWithdrawal {@code true} for the from-leg (DR control / CR liability transfer);
* {@code false} for the to-leg (DR liability transfer / CR control)
*/
/**
* Posts direct transfer journal entries between a current account and a counterpart savings/current account.
* Each entry carries the correct entity ID so the correct account number appears on each GL line.
*
* For withdrawal (current → savings):
* DR: current product GL (entity = current account)
* CR: counterpart product GL (entity = savings/current account)
*
* For deposit (savings → current):
* DR: counterpart product GL (entity = savings/current account)
* CR: current product GL (entity = current account)
*
* @param counterpartProductId savings/current product ID of the other account
* @param counterpartEntityId savings/current account ID of the other account
*/
private void postCurrentAccountTransferJournalEntries(final CurrentAccount account, final CurrentAccountTransaction txn,
final BigDecimal amount, final LocalDate txnDate, final boolean isWithdrawal,
final Long counterpartProductId, final Long counterpartEntityId) {
if (account.getProductId() == null || txn.getId() == null) {
return;
}
// Prefix "CA" so:
// 1. The helper does NOT re-prefix with "S" (avoids txn id collision with savings "S" prefix)
// 2. JournalEntryReadPlatformServiceImpl joins m_current_account (transaction_id LIKE 'CA%')
// so the correct current account number appears on the DR/CR entry.
final String currentTxnId = "CA" + txn.getId();
if (isWithdrawal) {
// DR: current account GL CR: counterpart (savings/current) GL
accountingProcessorHelper.createDirectTransferJournalEntriesForSavings(
account.getOffice(), account.getCurrencyCode(),
account.getProductId(), account.getId(),
counterpartProductId, counterpartEntityId,
currentTxnId, txnDate, amount);
} else {
// DR: counterpart (savings/current) GL CR: current account GL
accountingProcessorHelper.createDirectTransferJournalEntriesForSavings(
account.getOffice(), account.getCurrencyCode(),
counterpartProductId, counterpartEntityId,
account.getProductId(), account.getId(),
currentTxnId, txnDate, amount);
}
}
@Override
@Transactional
public CommandProcessingResult refundByTransfer(JsonCommand command) {
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.portfolio.account.starter;
import org.apache.fineract.accounting.journalentry.service.AccountingProcessorHelper;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
@@ -94,12 +95,13 @@ public class AccountConfiguration {
AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService,
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository,
CurrentAccountTransactionRepository currentAccountTransactionRepository) {
CurrentAccountTransactionRepository currentAccountTransactionRepository,
AccountingProcessorHelper accountingProcessorHelper) {
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties,
currentAccountRepository, currentAccountTransactionRepository);
currentAccountRepository, currentAccountTransactionRepository, accountingProcessorHelper);
}
@Bean
@@ -1,333 +1,356 @@
/**
* 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.domain;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsDepositBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsWithdrawalBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO;
import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException;
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainService {
private final PlatformSecurityContext context;
private final SavingsAccountRepositoryWrapper savingsAccountRepository;
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper;
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final ConfigurationDomainService configurationDomainService;
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
private final BusinessEventNotifierService businessEventNotifierService;
@Autowired
public SavingsAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository,
final SavingsAccountTransactionRepository savingsAccountTransactionRepository,
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper,
final JournalEntryWritePlatformService journalEntryWritePlatformService,
final ConfigurationDomainService configurationDomainService, final PlatformSecurityContext context,
final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
final BusinessEventNotifierService businessEventNotifierService) {
this.savingsAccountRepository = savingsAccountRepository;
this.savingsAccountTransactionRepository = savingsAccountTransactionRepository;
this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper;
this.journalEntryWritePlatformService = journalEntryWritePlatformService;
this.configurationDomainService = configurationDomainService;
this.context = context;
this.depositAccountOnHoldTransactionRepository = depositAccountOnHoldTransactionRepository;
this.businessEventNotifierService = businessEventNotifierService;
}
@Transactional
@Override
public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) {
context.authenticatedUser();
account.validateForAccountBlock();
account.validateForDebitBlock();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
final boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
if (transactionBooleanValues.isRegularTransaction() && !account.allowWithdrawal()) {
throw new DepositAccountTransactionNotAllowedException(account.getId(), "withdraw", account.depositAccountType());
}
final Set<Long> existingTransactionIds = new HashSet<>();
final LocalDate postInterestOnDate = null;
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
Integer accountType = null;
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
paymentDetail, null, accountType);
UUID refNo = UUID.randomUUID();
final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee(),
backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo.toString());
final MathContext mc = MathContext.DECIMAL64;
final LocalDate today = DateUtils.getBusinessLocalDate();
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, transactionBooleanValues.isInterestTransfer(), isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else {
account.calculateInterestUsing(mc, today, transactionBooleanValues.isInterestTransfer(),
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
postReversals);
}
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
}
account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(),
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill);
saveTransactionToGenerateTransactionId(withdrawal);
if (backdatedTxnsAllowedTill) {
// Update transactions separately
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
}
this.savingsAccountRepository.save(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, transactionBooleanValues.isAccountTransfer(),
backdatedTxnsAllowedTill);
businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal));
return withdrawal;
}
@Transactional
@Override
public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) {
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill);
}
private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final boolean isAccountTransfer, final boolean isRegularTransaction,
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) {
context.authenticatedUser();
account.validateForAccountBlock();
account.validateForCreditBlock();
// Global configurations
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
if (isRegularTransaction && !account.allowDeposit()) {
throw new DepositAccountTransactionNotAllowedException(account.getId(), "deposit", account.depositAccountType());
}
boolean isInterestTransfer = false;
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
Integer accountType = null;
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
paymentDetail, null, accountType);
UUID refNo = UUID.randomUUID();
final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType, backdatedTxnsAllowedTill,
relaxingDaysConfigForPivotDate, refNo.toString());
final LocalDate postInterestOnDate = null;
final MathContext mc = MathContext.DECIMAL64;
final LocalDate today = DateUtils.getBusinessLocalDate();
boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else {
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
}
saveTransactionToGenerateTransactionId(deposit);
if (backdatedTxnsAllowedTill) {
// Update transactions separately
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
}
this.savingsAccountRepository.saveAndFlush(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit));
return deposit;
}
@Transactional
@Override
public SavingsAccountTransaction handleHold(final SavingsAccount account, BigDecimal amount, LocalDate transactionDate,
Boolean lienAllowed) {
return SavingsAccountTransaction.holdAmount(account, account.office(), null, transactionDate,
Money.of(account.getCurrency(), amount), lienAllowed);
}
@Override
public SavingsAccountTransaction handleDividendPayout(final SavingsAccount account, final LocalDate transactionDate,
final BigDecimal transactionAmount, final boolean backdatedTxnsAllowedTill) {
final DateTimeFormatter fmt = null;
final PaymentDetail paymentDetail = null;
final boolean isAccountTransfer = false;
final boolean isRegularTransaction = true;
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DIVIDEND_PAYOUT;
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill);
}
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds,
Set<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(account.findExistingTransactionIds());
existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds());
}
private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) {
this.savingsAccountTransactionRepository.saveAndFlush(transaction);
return transaction.getId();
}
private void saveUpdatedTransactionsOfSavingsAccount(final List<SavingsAccountTransaction> savingsAccountTransactions) {
this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions);
}
private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set<Long> existingTransactionIds,
Set<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig());
existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig());
}
private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds,
final Set<Long> existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) {
final Map<String, Object> accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
}
@Transactional
@Override
public void postJournalEntries(final SavingsAccount account, final Set<Long> existingTransactionIds,
final Set<Long> existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) {
final boolean isAccountTransfer = false;
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
}
@Override
public SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
boolean backdatedTxnsAllowedTill) {
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
final boolean postReversals = true;
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
List<SavingsAccountTransaction> newTransactions = new ArrayList<>();
SavingsAccountTransaction reversal = null;
Set<SavingsAccountChargePaidBy> chargePaidBySet = null;
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
reversal = SavingsAccountTransaction.reversal(savingsAccountTransaction);
chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid();
reversal.getSavingsAccountChargesPaid().addAll(chargePaidBySet);
account.undoTransaction(savingsAccountTransaction);
if (postReversals) {
newTransactions.add(reversal);
}
}
boolean isInterestTransfer = false;
LocalDate postInterestOnDate = null;
final LocalDate today = DateUtils.getBusinessLocalDate();
final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode());
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
if (savingsAccountTransaction.isPostInterestCalculationRequired()
&& account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else {
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
}
account.validatePivotDateTransaction(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill,
relaxingDaysConfigForPivotDate, "savingsaccount");
account.validateAccountBalanceDoesNotBecomeNegativeMinimal(savingsAccountTransaction.getAmount(), false);
account.activateAccountBasedOnBalance();
}
this.savingsAccountRepository.save(account);
newTransactions.addAll(account.getSavingsAccountTransactionsWithPivotConfig());
this.savingsAccountTransactionRepository.saveAll(newTransactions);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, backdatedTxnsAllowedTill);
return reversal;
}
}
/**
* 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.domain;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsDepositBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsWithdrawalBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO;
import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException;
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainService {
private final PlatformSecurityContext context;
private final SavingsAccountRepositoryWrapper savingsAccountRepository;
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper;
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final ConfigurationDomainService configurationDomainService;
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
private final BusinessEventNotifierService businessEventNotifierService;
@Autowired
public SavingsAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository,
final SavingsAccountTransactionRepository savingsAccountTransactionRepository,
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper,
final JournalEntryWritePlatformService journalEntryWritePlatformService,
final ConfigurationDomainService configurationDomainService, final PlatformSecurityContext context,
final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
final BusinessEventNotifierService businessEventNotifierService) {
this.savingsAccountRepository = savingsAccountRepository;
this.savingsAccountTransactionRepository = savingsAccountTransactionRepository;
this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper;
this.journalEntryWritePlatformService = journalEntryWritePlatformService;
this.configurationDomainService = configurationDomainService;
this.context = context;
this.depositAccountOnHoldTransactionRepository = depositAccountOnHoldTransactionRepository;
this.businessEventNotifierService = businessEventNotifierService;
}
@Transactional
@Override
public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) {
context.authenticatedUser();
account.validateForAccountBlock();
account.validateForDebitBlock();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
final boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
if (transactionBooleanValues.isRegularTransaction() && !account.allowWithdrawal()) {
throw new DepositAccountTransactionNotAllowedException(account.getId(), "withdraw", account.depositAccountType());
}
final Set<Long> existingTransactionIds = new HashSet<>();
final LocalDate postInterestOnDate = null;
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
Integer accountType = null;
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
paymentDetail, null, accountType);
UUID refNo = UUID.randomUUID();
final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee(),
backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo.toString());
final MathContext mc = MathContext.DECIMAL64;
final LocalDate today = DateUtils.getBusinessLocalDate();
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, transactionBooleanValues.isInterestTransfer(), isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else {
account.calculateInterestUsing(mc, today, transactionBooleanValues.isInterestTransfer(),
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
postReversals);
}
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
}
account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(),
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill);
saveTransactionToGenerateTransactionId(withdrawal);
if (backdatedTxnsAllowedTill) {
// Update transactions separately
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
}
this.savingsAccountRepository.save(account);
if (!transactionBooleanValues.isSkipAccountingPostings()) {
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds,
transactionBooleanValues.isAccountTransfer(), backdatedTxnsAllowedTill);
}
businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal));
return withdrawal;
}
@Transactional
@Override
public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) {
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill, false);
}
@Override
public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill,
final boolean skipAccountingPostings) {
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill, skipAccountingPostings);
}
private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final boolean isAccountTransfer, final boolean isRegularTransaction,
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) {
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill, false);
}
private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final boolean isAccountTransfer, final boolean isRegularTransaction,
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill,
final boolean skipAccountingPostings) {
context.authenticatedUser();
account.validateForAccountBlock();
account.validateForCreditBlock();
// Global configurations
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
if (isRegularTransaction && !account.allowDeposit()) {
throw new DepositAccountTransactionNotAllowedException(account.getId(), "deposit", account.depositAccountType());
}
boolean isInterestTransfer = false;
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
Integer accountType = null;
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
paymentDetail, null, accountType);
UUID refNo = UUID.randomUUID();
final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType, backdatedTxnsAllowedTill,
relaxingDaysConfigForPivotDate, refNo.toString());
final LocalDate postInterestOnDate = null;
final MathContext mc = MathContext.DECIMAL64;
final LocalDate today = DateUtils.getBusinessLocalDate();
boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else {
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
}
saveTransactionToGenerateTransactionId(deposit);
if (backdatedTxnsAllowedTill) {
// Update transactions separately
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
}
this.savingsAccountRepository.saveAndFlush(account);
if (!skipAccountingPostings) {
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
}
businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit));
return deposit;
}
@Transactional
@Override
public SavingsAccountTransaction handleHold(final SavingsAccount account, BigDecimal amount, LocalDate transactionDate,
Boolean lienAllowed) {
return SavingsAccountTransaction.holdAmount(account, account.office(), null, transactionDate,
Money.of(account.getCurrency(), amount), lienAllowed);
}
@Override
public SavingsAccountTransaction handleDividendPayout(final SavingsAccount account, final LocalDate transactionDate,
final BigDecimal transactionAmount, final boolean backdatedTxnsAllowedTill) {
final DateTimeFormatter fmt = null;
final PaymentDetail paymentDetail = null;
final boolean isAccountTransfer = false;
final boolean isRegularTransaction = true;
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DIVIDEND_PAYOUT;
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill);
}
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds,
Set<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(account.findExistingTransactionIds());
existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds());
}
private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) {
this.savingsAccountTransactionRepository.saveAndFlush(transaction);
return transaction.getId();
}
private void saveUpdatedTransactionsOfSavingsAccount(final List<SavingsAccountTransaction> savingsAccountTransactions) {
this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions);
}
private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set<Long> existingTransactionIds,
Set<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig());
existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig());
}
private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds,
final Set<Long> existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) {
final Map<String, Object> accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(),
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
}
@Transactional
@Override
public void postJournalEntries(final SavingsAccount account, final Set<Long> existingTransactionIds,
final Set<Long> existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) {
final boolean isAccountTransfer = false;
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
}
@Override
public SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
boolean backdatedTxnsAllowedTill) {
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
final boolean postReversals = true;
final Set<Long> existingTransactionIds = new HashSet<>();
final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
}
List<SavingsAccountTransaction> newTransactions = new ArrayList<>();
SavingsAccountTransaction reversal = null;
Set<SavingsAccountChargePaidBy> chargePaidBySet = null;
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
reversal = SavingsAccountTransaction.reversal(savingsAccountTransaction);
chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid();
reversal.getSavingsAccountChargesPaid().addAll(chargePaidBySet);
account.undoTransaction(savingsAccountTransaction);
if (postReversals) {
newTransactions.add(reversal);
}
}
boolean isInterestTransfer = false;
LocalDate postInterestOnDate = null;
final LocalDate today = DateUtils.getBusinessLocalDate();
final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode());
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
if (savingsAccountTransaction.isPostInterestCalculationRequired()
&& account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else {
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
}
account.validatePivotDateTransaction(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill,
relaxingDaysConfigForPivotDate, "savingsaccount");
account.validateAccountBalanceDoesNotBecomeNegativeMinimal(savingsAccountTransaction.getAmount(), false);
account.activateAccountBasedOnBalance();
}
this.savingsAccountRepository.save(account);
newTransactions.addAll(account.getSavingsAccountTransactionsWithPivotConfig());
this.savingsAccountTransactionRepository.saveAll(newTransactions);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, backdatedTxnsAllowedTill);
return reversal;
}
}
@@ -1,59 +1,70 @@
/**
* 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;
public class SavingsTransactionBooleanValues {
private final boolean isAccountTransfer;
private final boolean isRegularTransaction;
private final boolean isApplyWithdrawFee;
private final boolean isInterestTransfer;
private final boolean isExceptionForBalanceCheck;
public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction,
final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck) {
this.isAccountTransfer = isAccountTransfer;
this.isRegularTransaction = isRegularTransaction;
this.isApplyWithdrawFee = isApplyWithdrawFee;
this.isInterestTransfer = isInterestTransfer;
this.isExceptionForBalanceCheck = isExceptionForBalanceCheck;
}
public boolean isAccountTransfer() {
return this.isAccountTransfer;
}
public boolean isRegularTransaction() {
return this.isRegularTransaction;
}
public boolean isApplyWithdrawFee() {
return this.isApplyWithdrawFee;
}
public boolean isInterestTransfer() {
return this.isInterestTransfer;
}
public boolean isExceptionForBalanceCheck() {
return this.isExceptionForBalanceCheck;
}
}
/**
* 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;
public class SavingsTransactionBooleanValues {
private final boolean isAccountTransfer;
private final boolean isRegularTransaction;
private final boolean isApplyWithdrawFee;
private final boolean isInterestTransfer;
private final boolean isExceptionForBalanceCheck;
private final boolean skipAccountingPostings;
public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction,
final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck) {
this(isAccountTransfer, isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isExceptionForBalanceCheck, false);
}
public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction,
final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck,
final boolean skipAccountingPostings) {
this.isAccountTransfer = isAccountTransfer;
this.isRegularTransaction = isRegularTransaction;
this.isApplyWithdrawFee = isApplyWithdrawFee;
this.isInterestTransfer = isInterestTransfer;
this.isExceptionForBalanceCheck = isExceptionForBalanceCheck;
this.skipAccountingPostings = skipAccountingPostings;
}
public boolean isAccountTransfer() {
return this.isAccountTransfer;
}
public boolean isRegularTransaction() {
return this.isRegularTransaction;
}
public boolean isApplyWithdrawFee() {
return this.isApplyWithdrawFee;
}
public boolean isInterestTransfer() {
return this.isInterestTransfer;
}
public boolean isExceptionForBalanceCheck() {
return this.isExceptionForBalanceCheck;
}
public boolean isSkipAccountingPostings() {
return this.skipAccountingPostings;
}
}
@@ -1,51 +1,55 @@
/**
* 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 java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Set;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
public interface SavingsAccountDomainService {
SavingsAccountTransaction handleWithdrawal(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
boolean backdatedTxnsAllowedTill);
void postJournalEntries(SavingsAccount savingsAccount, Set<Long> existingTransactionIds, Set<Long> existingReversedTransactionIds,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleHold(SavingsAccount account, BigDecimal amount, LocalDate transactionDate, Boolean lienAllowed);
}
/**
* 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 java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Set;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
public interface SavingsAccountDomainService {
SavingsAccountTransaction handleWithdrawal(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
boolean backdatedTxnsAllowedTill, boolean skipAccountingPostings);
void postJournalEntries(SavingsAccount savingsAccount, Set<Long> existingTransactionIds, Set<Long> existingReversedTransactionIds,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleHold(SavingsAccount account, BigDecimal amount, LocalDate transactionDate, Boolean lienAllowed);
}