diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
index ee76982f5..c2ffffe6d 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
@@ -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.
*
diff --git a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java
index 519edb532..66f577291 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/JournalEntryReadPlatformServiceImpl.java
@@ -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 ")
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsConfig.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsConfig.java
index 69fecbdb4..d7a120589 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsConfig.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsConfig.java
@@ -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);
+ }
+}
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java
index 352c012e3..cb3f87297 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteStandingInstructionsTasklet.java
@@ -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());
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java
index aaf24bd3d..1ae645c56 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java
@@ -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.
+ *
+ * Current Product GL mappings are stored under {@code PortfolioProductType.SAVING}, so the
+ * savings JE helper resolves them correctly.
+ *
+ * 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) {
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/starter/AccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/starter/AccountConfiguration.java
index c2bfdc826..58be11f63 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/starter/AccountConfiguration.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/account/starter/AccountConfiguration.java
@@ -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
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java
index fb16621dc..80b9c1a0d 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/domain/SavingsAccountDomainServiceJpa.java
@@ -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 existingTransactionIds = new HashSet<>();
- final LocalDate postInterestOnDate = null;
- final Set 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 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 existingTransactionIds = new HashSet<>();
- final Set 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 existingTransactionIds,
- Set 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 savingsAccountTransactions) {
- this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions);
- }
-
- private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set existingTransactionIds,
- Set existingReversedTransactionIds) {
- existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig());
- existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig());
- }
-
- private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds,
- final Set existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) {
-
- final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(),
- existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
- this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
- }
-
- @Transactional
- @Override
- public void postJournalEntries(final SavingsAccount account, final Set existingTransactionIds,
- final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) {
-
- final boolean isAccountTransfer = false;
- postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
- }
-
- @Override
- public SavingsAccountTransaction handleReversal(SavingsAccount account, List 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 existingTransactionIds = new HashSet<>();
- final Set existingReversedTransactionIds = new HashSet<>();
-
- if (backdatedTxnsAllowedTill) {
- updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
- } else {
- updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
- }
- List newTransactions = new ArrayList<>();
- SavingsAccountTransaction reversal = null;
-
- Set 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 existingTransactionIds = new HashSet<>();
+ final LocalDate postInterestOnDate = null;
+ final Set 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 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 existingTransactionIds = new HashSet<>();
+ final Set 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 existingTransactionIds,
+ Set 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 savingsAccountTransactions) {
+ this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions);
+ }
+
+ private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set existingTransactionIds,
+ Set existingReversedTransactionIds) {
+ existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig());
+ existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig());
+ }
+
+ private void postJournalEntries(final SavingsAccount savingsAccount, final Set existingTransactionIds,
+ final Set existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) {
+
+ final Map accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(),
+ existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
+ this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
+ }
+
+ @Transactional
+ @Override
+ public void postJournalEntries(final SavingsAccount account, final Set existingTransactionIds,
+ final Set existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) {
+
+ final boolean isAccountTransfer = false;
+ postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
+ }
+
+ @Override
+ public SavingsAccountTransaction handleReversal(SavingsAccount account, List 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 existingTransactionIds = new HashSet<>();
+ final Set existingReversedTransactionIds = new HashSet<>();
+
+ if (backdatedTxnsAllowedTill) {
+ updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
+ } else {
+ updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
+ }
+ List newTransactions = new ArrayList<>();
+ SavingsAccountTransaction reversal = null;
+
+ Set 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;
+ }
+}
diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/SavingsTransactionBooleanValues.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/SavingsTransactionBooleanValues.java
index 2cef4adfc..f3f8a79cb 100644
--- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/SavingsTransactionBooleanValues.java
+++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/SavingsTransactionBooleanValues.java
@@ -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;
+ }
+
+}
diff --git a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java
index 325c9dd8a..bdd17f5b2 100644
--- a/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java
+++ b/fineract-savings/src/main/java/org/apache/fineract/portfolio/savings/service/SavingsAccountDomainService.java
@@ -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 existingTransactionIds, Set existingReversedTransactionIds,
- boolean backdatedTxnsAllowedTill);
-
- SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount,
- boolean backdatedTxnsAllowedTill);
-
- SavingsAccountTransaction handleReversal(SavingsAccount account, List 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 existingTransactionIds, Set existingReversedTransactionIds,
+ boolean backdatedTxnsAllowedTill);
+
+ SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount,
+ boolean backdatedTxnsAllowedTill);
+
+ SavingsAccountTransaction handleReversal(SavingsAccount account, List savingsAccountTransactions,
+ boolean backdatedTxnsAllowedTill);
+
+ SavingsAccountTransaction handleHold(SavingsAccount account, BigDecimal amount, LocalDate transactionDate, Boolean lienAllowed);
+}