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:
co-authored by
Claude Sonnet 4.6
parent
2251fd2259
commit
7d4f409202
+21
@@ -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.
|
||||
*
|
||||
|
||||
+3
-4
@@ -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 ")
|
||||
|
||||
+4
-1
@@ -20,6 +20,7 @@ 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;
|
||||
@@ -49,6 +50,8 @@ public class ExecuteStandingInstructionsConfig {
|
||||
private DatabaseSpecificSQLGenerator sqlGenerator;
|
||||
@Autowired
|
||||
private AccountTransfersWritePlatformService accountTransfersWritePlatformService;
|
||||
@Autowired
|
||||
private AccountTransferDetailRepository accountTransferDetailRepository;
|
||||
|
||||
@Bean
|
||||
protected Step executeStandingInstructionsStep() {
|
||||
@@ -65,6 +68,6 @@ public class ExecuteStandingInstructionsConfig {
|
||||
@Bean
|
||||
public ExecuteStandingInstructionsTasklet executeStandingInstructionsTasklet() {
|
||||
return new ExecuteStandingInstructionsTasklet(standingInstructionReadPlatformService, jdbcTemplate, sqlGenerator,
|
||||
accountTransfersWritePlatformService);
|
||||
accountTransfersWritePlatformService, accountTransferDetailRepository);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -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());
|
||||
|
||||
|
||||
+147
-35
@@ -34,6 +34,9 @@ import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.CashAccountsForSavings;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.FinancialActivity;
|
||||
import org.apache.fineract.accounting.journalentry.service.AccountingProcessorHelper;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
@@ -96,6 +99,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
private final FineractProperties fineractProperties;
|
||||
private final CurrentAccountRepository currentAccountRepository;
|
||||
private final CurrentAccountTransactionRepository currentAccountTransactionRepository;
|
||||
private final AccountingProcessorHelper accountingProcessorHelper;
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
@@ -467,23 +471,29 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
.assembleFrom(accountTransferDTO.getToAccountId(), backdatedTxnsAllowedTill);
|
||||
|
||||
final CurrentAccountTransaction withdrawal = performCurrentWithdrawal(fromCurrentAccount,
|
||||
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getTransactionDate());
|
||||
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getTransactionDate(),
|
||||
toSavingsAccount.productId(), toSavingsAccount.getId());
|
||||
|
||||
// skipAccountingPostings=true: current account withdrawal leg (C-txn) already posts
|
||||
// DR CURRENT ACCOUNT / CR SAVING INDIVIDUAL directly — each with its own account number.
|
||||
// Savings deposit GL entries would produce a self-cancelling DR+CR on SAVING INDIVIDUAL — suppress them.
|
||||
final SavingsAccountTransaction deposit = this.savingsAccountDomainService.handleDeposit(toSavingsAccount,
|
||||
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(),
|
||||
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
|
||||
isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill);
|
||||
isAccountTransfer, isRegularTransaction, backdatedTxnsAllowedTill, true);
|
||||
|
||||
final AccountTransferDetails newDetails = AccountTransferDetails.currentToSavingsTransfer(
|
||||
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
|
||||
toSavingsAccount.office(), toSavingsAccount.getClient(), toSavingsAccount,
|
||||
accountTransferDTO.getTransferType());
|
||||
if (accountTransferDetails == null) {
|
||||
accountTransferDetails = AccountTransferDetails.currentToSavingsTransfer(
|
||||
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
|
||||
toSavingsAccount.office(), toSavingsAccount.getClient(), toSavingsAccount,
|
||||
accountTransferDTO.getTransferType());
|
||||
}
|
||||
final Money amount = Money.of(toSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount());
|
||||
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
|
||||
newDetails, null, deposit, accountTransferDTO.getTransactionDate(), amount,
|
||||
accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
|
||||
accountTransferDetails, null, deposit, accountTransferDTO.getTransactionDate(), amount,
|
||||
accountTransferDTO.getDescription()));
|
||||
this.accountTransferDetailRepository.saveAndFlush(newDetails);
|
||||
transferTransactionId = newDetails.getId();
|
||||
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
|
||||
transferTransactionId = accountTransferDetails.getId();
|
||||
|
||||
} else if (isSavingsToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
|
||||
// --- Savings Account → Current Account ---
|
||||
@@ -494,27 +504,33 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
.orElseThrow(() -> new GeneralPlatformDomainRuleException(
|
||||
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId()));
|
||||
|
||||
// skipAccountingPostings=true: current account deposit leg (C-txn) posts
|
||||
// DR SAVING INDIVIDUAL / CR CURRENT ACCOUNT directly. Savings withdrawal GL entries
|
||||
// would produce a self-cancelling DR+CR on the same SAVING INDIVIDUAL GL — suppress them.
|
||||
final SavingsTransactionBooleanValues txnBooleans = new SavingsTransactionBooleanValues(isAccountTransfer,
|
||||
isRegularTransaction, fromSavingsAccount.isWithdrawalFeeApplicableForTransfer(), false,
|
||||
accountTransferDTO.isExceptionForBalanceCheck());
|
||||
accountTransferDTO.isExceptionForBalanceCheck(), true);
|
||||
final SavingsAccountTransaction withdrawal = this.savingsAccountDomainService.handleWithdrawal(fromSavingsAccount,
|
||||
accountTransferDTO.getFmt(), accountTransferDTO.getTransactionDate(),
|
||||
accountTransferDTO.getTransactionAmount(), accountTransferDTO.getPaymentDetail(),
|
||||
txnBooleans, backdatedTxnsAllowedTill);
|
||||
|
||||
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
||||
accountTransferDTO.getTransactionDate());
|
||||
accountTransferDTO.getTransactionDate(),
|
||||
fromSavingsAccount.productId(), fromSavingsAccount.getId());
|
||||
|
||||
final AccountTransferDetails newDetails = AccountTransferDetails.savingsToCurrentTransfer(
|
||||
fromSavingsAccount.office(), fromSavingsAccount.getClient(), fromSavingsAccount,
|
||||
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
|
||||
accountTransferDTO.getTransferType());
|
||||
if (accountTransferDetails == null) {
|
||||
accountTransferDetails = AccountTransferDetails.savingsToCurrentTransfer(
|
||||
fromSavingsAccount.office(), fromSavingsAccount.getClient(), fromSavingsAccount,
|
||||
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
|
||||
accountTransferDTO.getTransferType());
|
||||
}
|
||||
final Money amount = Money.of(fromSavingsAccount.getCurrency(), accountTransferDTO.getTransactionAmount());
|
||||
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
|
||||
newDetails, withdrawal, null, accountTransferDTO.getTransactionDate(), amount,
|
||||
accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
|
||||
accountTransferDetails, withdrawal, null, accountTransferDTO.getTransactionDate(), amount,
|
||||
accountTransferDTO.getDescription()));
|
||||
this.accountTransferDetailRepository.saveAndFlush(newDetails);
|
||||
transferTransactionId = newDetails.getId();
|
||||
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
|
||||
transferTransactionId = accountTransferDetails.getId();
|
||||
|
||||
} else if (isCurrentToCurrentAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
|
||||
// --- Current Account → Current Account ---
|
||||
@@ -528,22 +544,26 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getToAccountId()));
|
||||
|
||||
performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
||||
accountTransferDTO.getTransactionDate());
|
||||
accountTransferDTO.getTransactionDate(),
|
||||
toCurrentAccount.getProductId(), toCurrentAccount.getId());
|
||||
performCurrentDeposit(toCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
||||
accountTransferDTO.getTransactionDate());
|
||||
accountTransferDTO.getTransactionDate(),
|
||||
fromCurrentAccount.getProductId(), fromCurrentAccount.getId());
|
||||
|
||||
final AccountTransferDetails newDetails = AccountTransferDetails.currentToCurrentTransfer(
|
||||
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
|
||||
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
|
||||
accountTransferDTO.getTransferType());
|
||||
if (accountTransferDetails == null) {
|
||||
accountTransferDetails = AccountTransferDetails.currentToCurrentTransfer(
|
||||
fromCurrentAccount.getOffice(), fromCurrentAccount.getClient(), fromCurrentAccount,
|
||||
toCurrentAccount.getOffice(), toCurrentAccount.getClient(), toCurrentAccount,
|
||||
accountTransferDTO.getTransferType());
|
||||
}
|
||||
final MonetaryCurrency currency = new MonetaryCurrency(fromCurrentAccount.getCurrencyCode(),
|
||||
fromCurrentAccount.getCurrencyDigits(), fromCurrentAccount.getCurrencyMultiplesOf());
|
||||
final Money amount = Money.of(currency, accountTransferDTO.getTransactionAmount());
|
||||
newDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
|
||||
newDetails, null, null, accountTransferDTO.getTransactionDate(), amount,
|
||||
accountTransferDetails.addAccountTransferTransaction(AccountTransferTransaction.savingsToSavingsTransfer(
|
||||
accountTransferDetails, null, null, accountTransferDTO.getTransactionDate(), amount,
|
||||
accountTransferDTO.getDescription()));
|
||||
this.accountTransferDetailRepository.saveAndFlush(newDetails);
|
||||
transferTransactionId = newDetails.getId();
|
||||
this.accountTransferDetailRepository.saveAndFlush(accountTransferDetails);
|
||||
transferTransactionId = accountTransferDetails.getId();
|
||||
|
||||
} else if (isCurrentToLoanAccountTransfer(accountTransferDTO.getFromAccountType(), accountTransferDTO.getToAccountType())) {
|
||||
// --- Current Account → Loan Account ---
|
||||
@@ -553,7 +573,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
"error.msg.currentaccount.not.found", "Current account not found: " + accountTransferDTO.getFromAccountId()));
|
||||
final Loan toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId());
|
||||
|
||||
performCurrentWithdrawal(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
||||
performCurrentWithdrawalForLoan(fromCurrentAccount, accountTransferDTO.getTransactionAmount(),
|
||||
accountTransferDTO.getTransactionDate());
|
||||
|
||||
ExternalId externalId = externalIdFactory.create(accountTransferDTO.getTxnExternalId().getValue());
|
||||
@@ -656,10 +676,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw amount from a current account and save the transaction.
|
||||
* Throws {@link GeneralPlatformDomainRuleException} if balance is insufficient.
|
||||
* Withdraw for Current → Loan transfers. Loan domain service handles the loan-side GL,
|
||||
* so current account side uses LIABILITY_TRANSFER as the counterpart (classic approach).
|
||||
*/
|
||||
private CurrentAccountTransaction performCurrentWithdrawal(final CurrentAccount account, final BigDecimal amount,
|
||||
private CurrentAccountTransaction performCurrentWithdrawalForLoan(final CurrentAccount account, final BigDecimal amount,
|
||||
final LocalDate txnDate) {
|
||||
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
|
||||
final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO;
|
||||
@@ -677,6 +697,40 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
account.setTotalWithdrawalsDerived(
|
||||
(account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount));
|
||||
currentAccountRepository.save(account);
|
||||
if (account.getProductId() != null && txn.getId() != null) {
|
||||
final String currentTxnId = "CA" + txn.getId();
|
||||
accountingProcessorHelper.createCashBasedJournalEntriesAndReversalsForSavings(account.getOffice(),
|
||||
account.getCurrencyCode(), CashAccountsForSavings.SAVINGS_CONTROL.getValue(),
|
||||
FinancialActivity.LIABILITY_TRANSFER.getValue(), account.getProductId(), null,
|
||||
account.getId(), currentTxnId, txnDate, amount, false);
|
||||
}
|
||||
return txn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Withdraw amount from a current account and save the transaction.
|
||||
* Throws {@link GeneralPlatformDomainRuleException} if balance is insufficient.
|
||||
*/
|
||||
private CurrentAccountTransaction performCurrentWithdrawal(final CurrentAccount account, final BigDecimal amount,
|
||||
final LocalDate txnDate, final Long counterpartProductId, final Long counterpartEntityId) {
|
||||
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
|
||||
final BigDecimal onHold = account.getOnHoldFundsDerived() != null ? account.getOnHoldFundsDerived() : BigDecimal.ZERO;
|
||||
final BigDecimal available = balance.subtract(onHold);
|
||||
if (!account.isAllowOverdraft() && amount.compareTo(available) > 0) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.current.account.insufficient.balance",
|
||||
"Insufficient balance in current account " + account.getId()
|
||||
+ ". Available: " + available + ", Requested: " + amount, account.getId());
|
||||
}
|
||||
final CurrentAccountTransaction txn = CurrentAccountTransaction.withdrawal(account, account.getOffice(), amount, txnDate, null);
|
||||
final BigDecimal newBalance = balance.subtract(amount);
|
||||
txn.setRunningBalanceDerived(newBalance);
|
||||
currentAccountTransactionRepository.saveAndFlush(txn);
|
||||
account.setAccountBalanceDerived(newBalance);
|
||||
account.setTotalWithdrawalsDerived(
|
||||
(account.getTotalWithdrawalsDerived() != null ? account.getTotalWithdrawalsDerived() : BigDecimal.ZERO).add(amount));
|
||||
currentAccountRepository.save(account);
|
||||
// Post journal entries: DR current account GL, CR counterpart GL — each with correct entity/account number
|
||||
postCurrentAccountTransferJournalEntries(account, txn, amount, txnDate, true, counterpartProductId, counterpartEntityId);
|
||||
return txn;
|
||||
}
|
||||
|
||||
@@ -684,7 +738,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
* Deposit amount into a current account and save the transaction.
|
||||
*/
|
||||
private CurrentAccountTransaction performCurrentDeposit(final CurrentAccount account, final BigDecimal amount,
|
||||
final LocalDate txnDate) {
|
||||
final LocalDate txnDate, final Long counterpartProductId, final Long counterpartEntityId) {
|
||||
final BigDecimal balance = account.getAccountBalanceDerived() != null ? account.getAccountBalanceDerived() : BigDecimal.ZERO;
|
||||
final CurrentAccountTransaction txn = CurrentAccountTransaction.deposit(account, account.getOffice(), amount, txnDate, null);
|
||||
final BigDecimal newBalance = balance.add(amount);
|
||||
@@ -694,9 +748,67 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
|
||||
account.setTotalDepositsDerived(
|
||||
(account.getTotalDepositsDerived() != null ? account.getTotalDepositsDerived() : BigDecimal.ZERO).add(amount));
|
||||
currentAccountRepository.save(account);
|
||||
// Post journal entries: DR counterpart GL, CR current account GL — each with correct entity/account number
|
||||
postCurrentAccountTransferJournalEntries(account, txn, amount, txnDate, false, counterpartProductId, counterpartEntityId);
|
||||
return txn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts double-entry journal entries for a current account leg of an account transfer.
|
||||
* <p>
|
||||
* Current Product GL mappings are stored under {@code PortfolioProductType.SAVING}, so the
|
||||
* savings JE helper resolves them correctly.
|
||||
* <p>
|
||||
* Transaction id is prefixed with "C" (e.g. {@code C57}) so the entries are NOT grouped with
|
||||
* savings entries that share the same numeric id (those use prefix "S"). The helper only
|
||||
* appends its own "S" prefix when the id is numeric; passing "C57" keeps it as-is.
|
||||
*
|
||||
* @param isWithdrawal {@code true} for the from-leg (DR control / CR liability transfer);
|
||||
* {@code false} for the to-leg (DR liability transfer / CR control)
|
||||
*/
|
||||
/**
|
||||
* Posts direct transfer journal entries between a current account and a counterpart savings/current account.
|
||||
* Each entry carries the correct entity ID so the correct account number appears on each GL line.
|
||||
*
|
||||
* For withdrawal (current → savings):
|
||||
* DR: current product GL (entity = current account)
|
||||
* CR: counterpart product GL (entity = savings/current account)
|
||||
*
|
||||
* For deposit (savings → current):
|
||||
* DR: counterpart product GL (entity = savings/current account)
|
||||
* CR: current product GL (entity = current account)
|
||||
*
|
||||
* @param counterpartProductId savings/current product ID of the other account
|
||||
* @param counterpartEntityId savings/current account ID of the other account
|
||||
*/
|
||||
private void postCurrentAccountTransferJournalEntries(final CurrentAccount account, final CurrentAccountTransaction txn,
|
||||
final BigDecimal amount, final LocalDate txnDate, final boolean isWithdrawal,
|
||||
final Long counterpartProductId, final Long counterpartEntityId) {
|
||||
if (account.getProductId() == null || txn.getId() == null) {
|
||||
return;
|
||||
}
|
||||
// Prefix "CA" so:
|
||||
// 1. The helper does NOT re-prefix with "S" (avoids txn id collision with savings "S" prefix)
|
||||
// 2. JournalEntryReadPlatformServiceImpl joins m_current_account (transaction_id LIKE 'CA%')
|
||||
// so the correct current account number appears on the DR/CR entry.
|
||||
final String currentTxnId = "CA" + txn.getId();
|
||||
if (isWithdrawal) {
|
||||
// DR: current account GL CR: counterpart (savings/current) GL
|
||||
accountingProcessorHelper.createDirectTransferJournalEntriesForSavings(
|
||||
account.getOffice(), account.getCurrencyCode(),
|
||||
account.getProductId(), account.getId(),
|
||||
counterpartProductId, counterpartEntityId,
|
||||
currentTxnId, txnDate, amount);
|
||||
} else {
|
||||
// DR: counterpart (savings/current) GL CR: current account GL
|
||||
accountingProcessorHelper.createDirectTransferJournalEntriesForSavings(
|
||||
account.getOffice(), account.getCurrencyCode(),
|
||||
counterpartProductId, counterpartEntityId,
|
||||
account.getProductId(), account.getId(),
|
||||
currentTxnId, txnDate, amount);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult refundByTransfer(JsonCommand command) {
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
+27
-4
@@ -139,8 +139,10 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi
|
||||
}
|
||||
this.savingsAccountRepository.save(account);
|
||||
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, transactionBooleanValues.isAccountTransfer(),
|
||||
backdatedTxnsAllowedTill);
|
||||
if (!transactionBooleanValues.isSkipAccountingPostings()) {
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds,
|
||||
transactionBooleanValues.isAccountTransfer(), backdatedTxnsAllowedTill);
|
||||
}
|
||||
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal));
|
||||
return withdrawal;
|
||||
@@ -153,13 +155,32 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi
|
||||
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);
|
||||
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();
|
||||
@@ -210,7 +231,9 @@ public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainServi
|
||||
|
||||
this.savingsAccountRepository.saveAndFlush(account);
|
||||
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
|
||||
if (!skipAccountingPostings) {
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
|
||||
}
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit));
|
||||
return deposit;
|
||||
}
|
||||
|
||||
+11
@@ -25,15 +25,22 @@ public class SavingsTransactionBooleanValues {
|
||||
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() {
|
||||
@@ -56,4 +63,8 @@ public class SavingsTransactionBooleanValues {
|
||||
return this.isExceptionForBalanceCheck;
|
||||
}
|
||||
|
||||
public boolean isSkipAccountingPostings() {
|
||||
return this.skipAccountingPostings;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
@@ -38,6 +38,10 @@ public interface SavingsAccountDomainService {
|
||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
|
||||
boolean backdatedTxnsAllowedTill);
|
||||
|
||||
SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
|
||||
boolean backdatedTxnsAllowedTill, boolean skipAccountingPostings);
|
||||
|
||||
void postJournalEntries(SavingsAccount savingsAccount, Set<Long> existingTransactionIds, Set<Long> existingReversedTransactionIds,
|
||||
boolean backdatedTxnsAllowedTill);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user