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); 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. * 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(" 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(" 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 COALESCE(ca.account_no, sa.account_no) ")
.append(" WHEN journalEntry.entity_type_enum = 2 THEN sa.account_no ")
.append(" ELSE NULL ") .append(" ELSE NULL ")
.append(" END as savingsAccountNo "); .append(" END as savingsAccountNo ");
if (associationParametersData.isRunningBalanceRequired()) { if (associationParametersData.isRunningBalanceRequired()) {
@@ -130,8 +129,8 @@ public class JournalEntryReadPlatformServiceImpl implements JournalEntryReadPlat
.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_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 AND journalEntry.transaction_id LIKE 'CA%' "); .append(" left join m_current_account ca on ca.id = journalEntry.entity_id AND journalEntry.entity_type_enum = 2 ");
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 ")
@@ -1,70 +1,73 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.account.jobs.executestandinginstructions; package org.apache.fineract.portfolio.account.jobs.executestandinginstructions;
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator; import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
import org.apache.fineract.infrastructure.jobs.service.JobName; import org.apache.fineract.infrastructure.jobs.service.JobName;
import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService; import org.apache.fineract.portfolio.account.domain.AccountTransferDetailRepository;
import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService; import org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
import org.springframework.batch.core.Job; import org.apache.fineract.portfolio.account.service.StandingInstructionReadPlatformService;
import org.springframework.batch.core.Step; import org.springframework.batch.core.Job;
import org.springframework.batch.core.job.builder.JobBuilder; import org.springframework.batch.core.Step;
import org.springframework.batch.core.launch.support.RunIdIncrementer; import org.springframework.batch.core.job.builder.JobBuilder;
import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.batch.core.repository.JobRepository;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.batch.core.step.builder.StepBuilder;
import org.springframework.context.annotation.Bean; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class ExecuteStandingInstructionsConfig { @Configuration
public class ExecuteStandingInstructionsConfig {
@Autowired
private JobRepository jobRepository; @Autowired
@Autowired private JobRepository jobRepository;
private PlatformTransactionManager transactionManager; @Autowired
@Autowired private PlatformTransactionManager transactionManager;
private StandingInstructionReadPlatformService standingInstructionReadPlatformService; @Autowired
@Autowired private StandingInstructionReadPlatformService standingInstructionReadPlatformService;
private JdbcTemplate jdbcTemplate; @Autowired
@Autowired private JdbcTemplate jdbcTemplate;
private DatabaseSpecificSQLGenerator sqlGenerator; @Autowired
@Autowired private DatabaseSpecificSQLGenerator sqlGenerator;
private AccountTransfersWritePlatformService accountTransfersWritePlatformService; @Autowired
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
@Bean @Autowired
protected Step executeStandingInstructionsStep() { private AccountTransferDetailRepository accountTransferDetailRepository;
return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository)
.tasklet(executeStandingInstructionsTasklet(), transactionManager).build(); @Bean
} protected Step executeStandingInstructionsStep() {
return new StepBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository)
@Bean .tasklet(executeStandingInstructionsTasklet(), transactionManager).build();
public Job executeStandingInstructionsJob() { }
return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep())
.incrementer(new RunIdIncrementer()).build(); @Bean
} public Job executeStandingInstructionsJob() {
return new JobBuilder(JobName.EXECUTE_STANDING_INSTRUCTIONS.name(), jobRepository).start(executeStandingInstructionsStep())
@Bean .incrementer(new RunIdIncrementer()).build();
public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() { }
return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator,
accountTransfersWritePlatformService); @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.AccountTransferDTO;
import org.apache.fineract.portfolio.account.data.StandingInstructionData; import org.apache.fineract.portfolio.account.data.StandingInstructionData;
import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData; 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.AccountTransferRecurrenceType;
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus; import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
import org.apache.fineract.portfolio.account.domain.StandingInstructionType; import org.apache.fineract.portfolio.account.domain.StandingInstructionType;
@@ -58,6 +60,7 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet {
private final JdbcTemplate jdbcTemplate; private final JdbcTemplate jdbcTemplate;
private final DatabaseSpecificSQLGenerator sqlGenerator; private final DatabaseSpecificSQLGenerator sqlGenerator;
private final AccountTransfersWritePlatformService accountTransfersWritePlatformService; private final AccountTransfersWritePlatformService accountTransfersWritePlatformService;
private final AccountTransferDetailRepository accountTransferDetailRepository;
@Override @Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
@@ -105,10 +108,15 @@ public class ExecuteStandingInstructionsTasklet implements Tasklet {
final SavingsAccount fromSavingsAccount = null; final SavingsAccount fromSavingsAccount = null;
final boolean isRegularTransaction = true; final boolean isRegularTransaction = true;
final boolean isExceptionForBalanceCheck = false; 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, AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, transactionAmount,
data.fromAccountTypeEnum(), data.toAccountTypeEnum(), data.getFromAccount().getId(), data.getToAccount().getId(), data.fromAccountTypeEnum(), data.toAccountTypeEnum(), data.getFromAccount().getId(), data.getToAccount().getId(),
data.getName() + " Standing instruction trasfer ", null, null, null, null, data.toTransferType(), null, null, 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); isRegularTransaction, isExceptionForBalanceCheck);
final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId()); final boolean transferCompleted = transferAmount(errors, accountTransferDTO, data.getId());
@@ -34,6 +34,9 @@ import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import lombok.RequiredArgsConstructor; 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.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.core.config.FineractProperties;
@@ -96,6 +99,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
private final FineractProperties fineractProperties; private final FineractProperties fineractProperties;
private final CurrentAccountRepository currentAccountRepository; private final CurrentAccountRepository currentAccountRepository;
private final CurrentAccountTransactionRepository currentAccountTransactionRepository; private final CurrentAccountTransactionRepository currentAccountTransactionRepository;
private final AccountingProcessorHelper accountingProcessorHelper;
@Transactional @Transactional
@Override @Override
@@ -467,23 +471,29 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
.assembleFrom(accountTransferDTO.getToAccountId(), backdatedTxnsAllowedTill); .assembleFrom(accountTransferDTO.getToAccountId(), backdatedTxnsAllowedTill);
final CurrentAccountTransaction withdrawal = performCurrentWithdrawal(fromCurrentAccount, 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, final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount,
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(),
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill); isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill, true);
final AccountTransferDetails newDetails = AccountTransferDetails.currentToSavingsTransfer( if (accountTransferDetails == null) {
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount, accountTransferDetails = AccountTransferDetails.currentToSavingsTransfer(
toSavingsAccount.office(), toSavingsAccount.getClient(), toSavingsAccount, fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
accountTransferDTO.getTransferType()); toSavingsAccount.office(), toSavingsAccount.getClient(), toSavingsAccount,
accountTransferDTO.getTransferType());
}
final Money amount = Money.of(toSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount()); final Money amount = Money.of(toSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount());
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer( accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
newDetails, null, deposit, accountTransferDTO.getTransactionDate(), amount, accountTransferDetails, null, deposit, accountTransferDTO.getTransactionDate(), amount,
accountTransferDTO.getDescription())); accountTransferDTO.getDescription()));
this.accountTransferDetailRepository.saveAndFlush(newDetails); this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
transferTransactionId = newDetails.getId(); transferTransactionId = accountTransferDetails.getId();
} else if (isSavingsToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { } else if (isSavingsToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
// --- Savings Account → Current Account --- // --- Savings Account → Current Account ---
@@ -494,27 +504,33 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
.orElseThrow(() -> new GeneralPlatformDomainRuleException( .orElseThrow(() -> new GeneralPlatformDomainRuleException(
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId())); "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, final SavingsTransactionBooleanValues txnBooleans = new SavingsTransactionBooleanValues(isAccountTransfer,
isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), false, isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), false,
accountTransferDTO.isExceptionForBalanceCheck()); accountTransferDTO.isExceptionForBalanceCheck(), true);
final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount, final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount,
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(),
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(), accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
txnBooleans, backdatedTxnsAllowedTill); txnBooleans, backdatedTxnsAllowedTill);
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(), performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getTransactionDate()); accountTransferDTO.getTransactionDate(),
fromSavingsAccount.productId(), fromSavingsAccount.getId());
final AccountTransferDetails newDetails = AccountTransferDetails.savingsToCurrentTransfer( if (accountTransferDetails == null) {
fromSavingsAccount.office(), fromSavingsAccount.getClient(), fromSavingsAccount, accountTransferDetails = AccountTransferDetails.savingsToCurrentTransfer(
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount, fromSavingsAccount.office(), fromSavingsAccount.getClient(), fromSavingsAccount,
accountTransferDTO.getTransferType()); toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
accountTransferDTO.getTransferType());
}
final Money amount = Money.of(fromSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount()); final Money amount = Money.of(fromSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount());
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer( accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
newDetails, withdrawal, null, accountTransferDTO.getTransactionDate(), amount, accountTransferDetails, withdrawal, null, accountTransferDTO.getTransactionDate(), amount,
accountTransferDTO.getDescription())); accountTransferDTO.getDescription()));
this.accountTransferDetailRepository.saveAndFlush(newDetails); this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
transferTransactionId = newDetails.getId(); transferTransactionId = accountTransferDetails.getId();
} else if (isCurrentToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { } else if (isCurrentToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
// --- Current Account → Current Account --- // --- Current Account → Current Account ---
@@ -528,22 +544,26 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId())); "error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId()));
performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(), performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getTransactionDate()); accountTransferDTO.getTransactionDate(),
toCurrentAccount.getProductId(), toCurrentAccount.getId());
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(), performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getTransactionDate()); accountTransferDTO.getTransactionDate(),
fromCurrentAccount.getProductId(), fromCurrentAccount.getId());
final AccountTransferDetails newDetails = AccountTransferDetails.currentToCurrentTransfer( if (accountTransferDetails == null) {
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount, accountTransferDetails = AccountTransferDetails.currentToCurrentTransfer(
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount, fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
accountTransferDTO.getTransferType()); toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
accountTransferDTO.getTransferType());
}
final MonetaryCurrency currency = new MonetaryCurrency(fromCurrentAccount.getCurrencyCode(), final MonetaryCurrency currency = new MonetaryCurrency(fromCurrentAccount.getCurrencyCode(),
fromCurrentAccount.getCurrencyDigits(), fromCurrentAccount.getCurrencyMultiplesOf()); fromCurrentAccount.getCurrencyDigits(), fromCurrentAccount.getCurrencyMultiplesOf());
final Money amount = Money.of(currency, accountTransferDTO.getTransactionAmount()); final Money amount = Money.of(currency, accountTransferDTO.getTransactionAmount());
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer( accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
newDetails, null, null, accountTransferDTO.getTransactionDate(), amount, accountTransferDetails, null, null, accountTransferDTO.getTransactionDate(), amount,
accountTransferDTO.getDescription())); accountTransferDTO.getDescription()));
this.accountTransferDetailRepository.saveAndFlush(newDetails); this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
transferTransactionId = newDetails.getId(); transferTransactionId = accountTransferDetails.getId();
} else if (isCurrentToLoanAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) { } else if (isCurrentToLoanAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
// --- Current Account → Loan Account --- // --- Current Account → Loan Account ---
@@ -553,7 +573,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId())); "error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId()));
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId()); final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId());
performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(), performCurrentWithdrawalForLoan(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
accountTransferDTO.getTransactionDate()); accountTransferDTO.getTransactionDate());
ExternalId externalId = externalIdFactory.create(accountTransferDTO.getTxnExternalId().getValue()); 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. * Withdraw for Current → Loan transfers. Loan domain service handles the loan-side GL,
* Throws {@link GeneralPlatformDomainRuleException} if balance is insufficient. * 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 LocalDate txnDate) {
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO; final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO; final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO;
@@ -677,6 +697,40 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
account.setTotalWithdrawalsDerived( account.setTotalWithdrawalsDerived(
(account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount)); (account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount));
currentAccountRepository.save(account); 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; return txn;
} }
@@ -684,7 +738,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
* Deposit amount into a current account and save the transaction. * Deposit amount into a current account and save the transaction.
*/ */
private CurrentAccountTransaction performCurrentDeposit(final CurrentAccount account, final BigDecimal amount, 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 BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
final CurrentAccountTransaction txn = CurrentAccountTransaction.deposit(account, account.getOffice(), amount, txnDate, null); final CurrentAccountTransaction txn = CurrentAccountTransaction.deposit(account, account.getOffice(), amount, txnDate, null);
final BigDecimal newBalance = balance.add(amount); final BigDecimal newBalance = balance.add(amount);
@@ -694,9 +748,67 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
account.setTotalDepositsDerived( account.setTotalDepositsDerived(
(account.getTotalDepositsDerived() != null ? account.getTotalDepositsDerived() : BigDecimal.ZERO).add(amount)); (account.getTotalDepositsDerived() != null ? account.getTotalDepositsDerived() : BigDecimal.ZERO).add(amount));
currentAccountRepository.save(account); 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; 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 @Override
@Transactional @Transactional
public CommandProcessingResult refundByTransfer(JsonCommand command) { public CommandProcessingResult refundByTransfer(JsonCommand command) {
@@ -18,6 +18,7 @@
*/ */
package org.apache.fineract.portfolio.account.starter; 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.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.config.FineractProperties; import org.apache.fineract.infrastructure.core.config.FineractProperties;
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
@@ -94,12 +95,13 @@ public class AccountConfiguration {
AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService, AccountTransferDetailRepository accountTransferDetailRepository, LoanReadPlatformService loanReadPlatformService,
GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory, GSIMRepositoy gsimRepository, ConfigurationDomainService configurationDomainService, ExternalIdFactory externalIdFactory,
FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository, FineractProperties fineractProperties, CurrentAccountRepository currentAccountRepository,
CurrentAccountTransactionRepository currentAccountTransactionRepository) { CurrentAccountTransactionRepository currentAccountTransactionRepository,
AccountingProcessorHelper accountingProcessorHelper) {
return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler, return new AccountTransfersWritePlatformServiceImpl(accountTransfersDataValidator, accountTransferAssembler,
accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler, accountTransferRepository, savingsAccountAssembler, savingsAccountDomainService, loanAccountAssembler,
loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService, loanAccountDomainService, savingsAccountWritePlatformService, accountTransferDetailRepository, loanReadPlatformService,
gsimRepository, configurationDomainService, externalIdFactory, fineractProperties, gsimRepository, configurationDomainService, externalIdFactory, fineractProperties,
currentAccountRepository, currentAccountTransactionRepository); currentAccountRepository, currentAccountTransactionRepository, accountingProcessorHelper);
} }
@Bean @Bean
@@ -1,333 +1,356 @@
/** /**
* Licensed to the Apache Software Foundation (ASF) under one * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.savings.domain; package org.apache.fineract.portfolio.savings.domain;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.MathContext; import java.math.MathContext;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.service.DateUtils; 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.SavingsDepositBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsWithdrawalBusinessEvent; 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.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType; import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO; import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO;
import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException; import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException;
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService; import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
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;
@Service @Service
public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainService { public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainService {
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final SavingsAccountRepositoryWrapper savingsAccountRepository; private final SavingsAccountRepositoryWrapper savingsAccountRepository;
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository; private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper; private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper;
private final JournalEntryWritePlatformService journalEntryWritePlatformService; private final JournalEntryWritePlatformService journalEntryWritePlatformService;
private final ConfigurationDomainService configurationDomainService; private final ConfigurationDomainService configurationDomainService;
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository; private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
@Autowired @Autowired
public SavingsAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository, public SavingsAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository,
final SavingsAccountTransactionRepository savingsAccountTransactionRepository, final SavingsAccountTransactionRepository savingsAccountTransactionRepository,
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper, final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper,
final JournalEntryWritePlatformService journalEntryWritePlatformService, final JournalEntryWritePlatformService journalEntryWritePlatformService,
final ConfigurationDomainService configurationDomainService, final PlatformSecurityContext context, final ConfigurationDomainService configurationDomainService, final PlatformSecurityContext context,
final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository, final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
final BusinessEventNotifierService businessEventNotifierService) { final BusinessEventNotifierService businessEventNotifierService) {
this.savingsAccountRepository = savingsAccountRepository; this.savingsAccountRepository = savingsAccountRepository;
this.savingsAccountTransactionRepository = savingsAccountTransactionRepository; this.savingsAccountTransactionRepository = savingsAccountTransactionRepository;
this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper; this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper;
this.journalEntryWritePlatformService = journalEntryWritePlatformService; this.journalEntryWritePlatformService = journalEntryWritePlatformService;
this.configurationDomainService = configurationDomainService; this.configurationDomainService = configurationDomainService;
this.context = context; this.context = context;
this.depositAccountOnHoldTransactionRepository = depositAccountOnHoldTransactionRepository; this.depositAccountOnHoldTransactionRepository = depositAccountOnHoldTransactionRepository;
this.businessEventNotifierService = businessEventNotifierService; this.businessEventNotifierService = businessEventNotifierService;
} }
@Transactional @Transactional
@Override @Override
public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt, public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt,
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) { final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) {
context.authenticatedUser(); context.authenticatedUser();
account.validateForAccountBlock(); account.validateForAccountBlock();
account.validateForDebitBlock(); account.validateForDebitBlock();
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
.isSavingsInterestPostingAtCurrentPeriodEnd(); .isSavingsInterestPostingAtCurrentPeriodEnd();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
final boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed(); final boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
if (transactionBooleanValues.isRegularTransaction() && !account.allowWithdrawal()) { if (transactionBooleanValues.isRegularTransaction() && !account.allowWithdrawal()) {
throw new DepositAccountTransactionNotAllowedException(account.getId(), "withdraw", account.depositAccountType()); throw new DepositAccountTransactionNotAllowedException(account.getId(), "withdraw", account.depositAccountType());
} }
final Set<Long> existingTransactionIds = new HashSet<>(); final Set<Long> existingTransactionIds = new HashSet<>();
final LocalDate postInterestOnDate = null; final LocalDate postInterestOnDate = null;
final Set<Long> existingReversedTransactionIds = new HashSet<>(); final Set<Long> existingReversedTransactionIds = new HashSet<>();
if (backdatedTxnsAllowedTill) { if (backdatedTxnsAllowedTill) {
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds); updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else { } else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
} }
Integer accountType = null; Integer accountType = null;
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
paymentDetail, null, accountType); paymentDetail, null, accountType);
UUID refNo = UUID.randomUUID(); UUID refNo = UUID.randomUUID();
final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee(), final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee(),
backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo.toString()); backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo.toString());
final MathContext mc = MathContext.DECIMAL64; final MathContext mc = MathContext.DECIMAL64;
final LocalDate today = DateUtils.getBusinessLocalDate(); final LocalDate today = DateUtils.getBusinessLocalDate();
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
account.postInterest(mc, today, transactionBooleanValues.isInterestTransfer(), isSavingsInterestPostingAtCurrentPeriodEnd, account.postInterest(mc, today, transactionBooleanValues.isInterestTransfer(), isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
} else { } else {
account.calculateInterestUsing(mc, today, transactionBooleanValues.isInterestTransfer(), account.calculateInterestUsing(mc, today, transactionBooleanValues.isInterestTransfer(),
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
postReversals); postReversals);
} }
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null; List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) { if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account); .findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
} }
account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(), account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(),
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill); depositAccountOnHoldTransactions, backdatedTxnsAllowedTill);
saveTransactionToGenerateTransactionId(withdrawal); saveTransactionToGenerateTransactionId(withdrawal);
if (backdatedTxnsAllowedTill) { if (backdatedTxnsAllowedTill) {
// Update transactions separately // Update transactions separately
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig()); saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
} }
this.savingsAccountRepository.save(account); this.savingsAccountRepository.save(account);
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, transactionBooleanValues.isAccountTransfer(), if (!transactionBooleanValues.isSkipAccountingPostings()) {
backdatedTxnsAllowedTill); postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds,
transactionBooleanValues.isAccountTransfer(), backdatedTxnsAllowedTill);
businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal)); }
return withdrawal;
} businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal));
return withdrawal;
@Transactional }
@Override
public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt, @Transactional
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, @Override
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) { public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT; final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) {
savingsAccountTransactionType, backdatedTxnsAllowedTill); final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
} 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, @Override
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) { public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
context.authenticatedUser(); final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
account.validateForAccountBlock(); final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill,
account.validateForCreditBlock(); final boolean skipAccountingPostings) {
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
// Global configurations return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService savingsAccountTransactionType, backdatedTxnsAllowedTill, skipAccountingPostings);
.isSavingsInterestPostingAtCurrentPeriodEnd(); }
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
if (isRegularTransaction && !account.allowDeposit()) { final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
throw new DepositAccountTransactionNotAllowedException(account.getId(), "deposit", account.depositAccountType()); final boolean isAccountTransfer, final boolean isRegularTransaction,
} final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) {
boolean isInterestTransfer = false; return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
final Set<Long> existingTransactionIds = new HashSet<>(); savingsAccountTransactionType, backdatedTxnsAllowedTill, false);
final Set<Long> existingReversedTransactionIds = new HashSet<>(); }
if (backdatedTxnsAllowedTill) { private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds); final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
} else { final boolean isAccountTransfer, final boolean isRegularTransaction,
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill,
} final boolean skipAccountingPostings) {
context.authenticatedUser();
Integer accountType = null; account.validateForAccountBlock();
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount, account.validateForCreditBlock();
paymentDetail, null, accountType);
UUID refNo = UUID.randomUUID(); // Global configurations
final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType, backdatedTxnsAllowedTill, final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
relaxingDaysConfigForPivotDate, refNo.toString()); .isSavingsInterestPostingAtCurrentPeriodEnd();
final LocalDate postInterestOnDate = null; final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final MathContext mc = MathContext.DECIMAL64; final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
if (isRegularTransaction && !account.allowDeposit()) {
final LocalDate today = DateUtils.getBusinessLocalDate(); throw new DepositAccountTransactionNotAllowedException(account.getId(), "deposit", account.depositAccountType());
boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed(); }
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) { boolean isInterestTransfer = false;
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, final Set<Long> existingTransactionIds = new HashSet<>();
postInterestOnDate, backdatedTxnsAllowedTill, postReversals); final Set<Long> existingReversedTransactionIds = new HashSet<>();
} else {
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, if (backdatedTxnsAllowedTill) {
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} } else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
saveTransactionToGenerateTransactionId(deposit); }
if (backdatedTxnsAllowedTill) { Integer accountType = null;
// Update transactions separately final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig()); paymentDetail, null, accountType);
} UUID refNo = UUID.randomUUID();
final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType, backdatedTxnsAllowedTill,
this.savingsAccountRepository.saveAndFlush(account); relaxingDaysConfigForPivotDate, refNo.toString());
final LocalDate postInterestOnDate = null;
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); final MathContext mc = MathContext.DECIMAL64;
businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit));
return deposit; final LocalDate today = DateUtils.getBusinessLocalDate();
} boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
@Transactional account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
@Override postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
public SavingsAccountTransaction handleHold(final SavingsAccount account, BigDecimal amount, LocalDate transactionDate, } else {
Boolean lienAllowed) { account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
return SavingsAccountTransaction.holdAmount(account, account.office(), null, transactionDate, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
Money.of(account.getCurrency(), amount), lienAllowed); }
}
saveTransactionToGenerateTransactionId(deposit);
@Override
public SavingsAccountTransaction handleDividendPayout(final SavingsAccount account, final LocalDate transactionDate, if (backdatedTxnsAllowedTill) {
final BigDecimal transactionAmount, final boolean backdatedTxnsAllowedTill) { // Update transactions separately
final DateTimeFormatter fmt = null; saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
final PaymentDetail paymentDetail = null; }
final boolean isAccountTransfer = false;
final boolean isRegularTransaction = true; this.savingsAccountRepository.saveAndFlush(account);
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DIVIDEND_PAYOUT;
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction, if (!skipAccountingPostings) {
savingsAccountTransactionType, backdatedTxnsAllowedTill); postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
} }
businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit));
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds, return deposit;
Set<Long> existingReversedTransactionIds) { }
existingTransactionIds.addAll(account.findExistingTransactionIds());
existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds()); @Transactional
} @Override
public SavingsAccountTransaction handleHold(final SavingsAccount account, BigDecimal amount, LocalDate transactionDate,
private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) { Boolean lienAllowed) {
this.savingsAccountTransactionRepository.saveAndFlush(transaction); return SavingsAccountTransaction.holdAmount(account, account.office(), null, transactionDate,
return transaction.getId(); Money.of(account.getCurrency(), amount), lienAllowed);
} }
private void saveUpdatedTransactionsOfSavingsAccount(final List<SavingsAccountTransaction> savingsAccountTransactions) { @Override
this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions); public SavingsAccountTransaction handleDividendPayout(final SavingsAccount account, final LocalDate transactionDate,
} final BigDecimal transactionAmount, final boolean backdatedTxnsAllowedTill) {
final DateTimeFormatter fmt = null;
private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set<Long> existingTransactionIds, final PaymentDetail paymentDetail = null;
Set<Long> existingReversedTransactionIds) { final boolean isAccountTransfer = false;
existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig()); final boolean isRegularTransaction = true;
existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig()); final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DIVIDEND_PAYOUT;
} return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
savingsAccountTransactionType, backdatedTxnsAllowedTill);
private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds, }
final Set<Long> existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) {
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds,
final Map<String, Object> accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(), Set<Long> existingReversedTransactionIds) {
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); existingTransactionIds.addAll(account.findExistingTransactionIds());
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData); existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds());
} }
@Transactional private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) {
@Override this.savingsAccountTransactionRepository.saveAndFlush(transaction);
public void postJournalEntries(final SavingsAccount account, final Set<Long> existingTransactionIds, return transaction.getId();
final Set<Long> existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) { }
final boolean isAccountTransfer = false; private void saveUpdatedTransactionsOfSavingsAccount(final List<SavingsAccountTransaction> savingsAccountTransactions) {
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill); this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions);
} }
@Override private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set<Long> existingTransactionIds,
public SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions, Set<Long> existingReversedTransactionIds) {
boolean backdatedTxnsAllowedTill) { existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig());
existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig());
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService }
.isSavingsInterestPostingAtCurrentPeriodEnd();
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth(); private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds,
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate(); final Set<Long> existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) {
final boolean postReversals = true;
final Set<Long> existingTransactionIds = new HashSet<>(); final Map<String, Object> accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(),
final Set<Long> existingReversedTransactionIds = new HashSet<>(); existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
if (backdatedTxnsAllowedTill) { }
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
} else { @Transactional
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds); @Override
} public void postJournalEntries(final SavingsAccount account, final Set<Long> existingTransactionIds,
List<SavingsAccountTransaction> newTransactions = new ArrayList<>(); final Set<Long> existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) {
SavingsAccountTransaction reversal = null;
final boolean isAccountTransfer = false;
Set<SavingsAccountChargePaidBy> chargePaidBySet = null; postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) { }
reversal = SavingsAccountTransaction.reversal(savingsAccountTransaction);
chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid(); @Override
reversal.getSavingsAccountChargesPaid().addAll(chargePaidBySet); public SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
account.undoTransaction(savingsAccountTransaction); boolean backdatedTxnsAllowedTill) {
if (postReversals) {
newTransactions.add(reversal); final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
} .isSavingsInterestPostingAtCurrentPeriodEnd();
} final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
boolean isInterestTransfer = false; final boolean postReversals = true;
LocalDate postInterestOnDate = null; final Set<Long> existingTransactionIds = new HashSet<>();
final LocalDate today = DateUtils.getBusinessLocalDate(); final Set<Long> existingReversedTransactionIds = new HashSet<>();
final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode());
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) { if (backdatedTxnsAllowedTill) {
if (savingsAccountTransaction.isPostInterestCalculationRequired() updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
&& account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill)) { } else {
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, }
postInterestOnDate, backdatedTxnsAllowedTill, postReversals); List<SavingsAccountTransaction> newTransactions = new ArrayList<>();
} else { SavingsAccountTransaction reversal = null;
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals); Set<SavingsAccountChargePaidBy> chargePaidBySet = null;
} for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
account.validatePivotDateTransaction(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill, reversal = SavingsAccountTransaction.reversal(savingsAccountTransaction);
relaxingDaysConfigForPivotDate, "savingsaccount"); chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid();
account.validateAccountBalanceDoesNotBecomeNegativeMinimal(savingsAccountTransaction.getAmount(), false); reversal.getSavingsAccountChargesPaid().addAll(chargePaidBySet);
account.activateAccountBasedOnBalance(); account.undoTransaction(savingsAccountTransaction);
} if (postReversals) {
this.savingsAccountRepository.save(account); newTransactions.add(reversal);
newTransactions.addAll(account.getSavingsAccountTransactionsWithPivotConfig()); }
this.savingsAccountTransactionRepository.saveAll(newTransactions); }
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, backdatedTxnsAllowedTill);
boolean isInterestTransfer = false;
return reversal; 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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.savings; package org.apache.fineract.portfolio.savings;
public class SavingsTransactionBooleanValues { public class SavingsTransactionBooleanValues {
private final boolean isAccountTransfer; private final boolean isAccountTransfer;
private final boolean isRegularTransaction; private final boolean isRegularTransaction;
private final boolean isApplyWithdrawFee; private final boolean isApplyWithdrawFee;
private final boolean isInterestTransfer; private final boolean isInterestTransfer;
private final boolean isExceptionForBalanceCheck; 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) { public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction,
final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck) {
this.isAccountTransfer = isAccountTransfer; this(isAccountTransfer, isRegularTransaction, isApplyWithdrawFee, isInterestTransfer, isExceptionForBalanceCheck, false);
this.isRegularTransaction = isRegularTransaction; }
this.isApplyWithdrawFee = isApplyWithdrawFee;
this.isInterestTransfer = isInterestTransfer; public SavingsTransactionBooleanValues(final boolean isAccountTransfer, final boolean isRegularTransaction,
this.isExceptionForBalanceCheck = isExceptionForBalanceCheck; final boolean isApplyWithdrawFee, final boolean isInterestTransfer, final boolean isExceptionForBalanceCheck,
} final boolean skipAccountingPostings) {
this.isAccountTransfer = isAccountTransfer;
public boolean isAccountTransfer() { this.isRegularTransaction = isRegularTransaction;
return this.isAccountTransfer; this.isApplyWithdrawFee = isApplyWithdrawFee;
} this.isInterestTransfer = isInterestTransfer;
this.isExceptionForBalanceCheck = isExceptionForBalanceCheck;
public boolean isRegularTransaction() { this.skipAccountingPostings = skipAccountingPostings;
return this.isRegularTransaction; }
}
public boolean isAccountTransfer() {
public boolean isApplyWithdrawFee() { return this.isAccountTransfer;
return this.isApplyWithdrawFee; }
}
public boolean isRegularTransaction() {
public boolean isInterestTransfer() { return this.isRegularTransaction;
return this.isInterestTransfer; }
}
public boolean isApplyWithdrawFee() {
public boolean isExceptionForBalanceCheck() { return this.isApplyWithdrawFee;
return this.isExceptionForBalanceCheck; }
}
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 * Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file * or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information * distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file * regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the * to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance * "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at * with the License. You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, * Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an * software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the * KIND, either express or implied. See the License for the
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
package org.apache.fineract.portfolio.savings.service; package org.apache.fineract.portfolio.savings.service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues; import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
import org.apache.fineract.portfolio.savings.domain.SavingsAccount; import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction; import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
public interface SavingsAccountDomainService { public interface SavingsAccountDomainService {
SavingsAccountTransaction handleWithdrawal(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate, SavingsAccountTransaction handleWithdrawal(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues, BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues,
boolean backdatedTxnsAllowedTill); boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate, SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction, BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
boolean backdatedTxnsAllowedTill); boolean backdatedTxnsAllowedTill);
void postJournalEntries(SavingsAccount savingsAccount, Set<Long> existingTransactionIds, Set<Long> existingReversedTransactionIds, SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
boolean backdatedTxnsAllowedTill); BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
boolean backdatedTxnsAllowedTill, boolean skipAccountingPostings);
SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount,
boolean backdatedTxnsAllowedTill); void postJournalEntries(SavingsAccount savingsAccount, Set<Long> existingTransactionIds, Set<Long> existingReversedTransactionIds,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
boolean backdatedTxnsAllowedTill); SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleHold(SavingsAccount account, BigDecimal amount, LocalDate transactionDate, Boolean lienAllowed);
} SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
boolean backdatedTxnsAllowedTill);
SavingsAccountTransaction handleHold(SavingsAccount account, BigDecimal amount, LocalDate transactionDate, Boolean lienAllowed);
}