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 ")
|
||||
|
||||
+73
-70
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+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
|
||||
|
||||
+356
-333
@@ -1,333 +1,356 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsDepositBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsWithdrawalBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO;
|
||||
import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainService {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final SavingsAccountRepositoryWrapper savingsAccountRepository;
|
||||
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
|
||||
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper;
|
||||
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
|
||||
private final ConfigurationDomainService configurationDomainService;
|
||||
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
|
||||
@Autowired
|
||||
public SavingsAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository,
|
||||
final SavingsAccountTransactionRepository savingsAccountTransactionRepository,
|
||||
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper,
|
||||
final JournalEntryWritePlatformService journalEntryWritePlatformService,
|
||||
final ConfigurationDomainService configurationDomainService, final PlatformSecurityContext context,
|
||||
final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
|
||||
final BusinessEventNotifierService businessEventNotifierService) {
|
||||
this.savingsAccountRepository = savingsAccountRepository;
|
||||
this.savingsAccountTransactionRepository = savingsAccountTransactionRepository;
|
||||
this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper;
|
||||
this.journalEntryWritePlatformService = journalEntryWritePlatformService;
|
||||
this.configurationDomainService = configurationDomainService;
|
||||
this.context = context;
|
||||
this.depositAccountOnHoldTransactionRepository = depositAccountOnHoldTransactionRepository;
|
||||
this.businessEventNotifierService = businessEventNotifierService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||
final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) {
|
||||
context.authenticatedUser();
|
||||
account.validateForAccountBlock();
|
||||
account.validateForDebitBlock();
|
||||
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
||||
.isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
|
||||
final boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
|
||||
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
|
||||
if (transactionBooleanValues.isRegularTransaction() && !account.allowWithdrawal()) {
|
||||
throw new DepositAccountTransactionNotAllowedException(account.getId(), "withdraw", account.depositAccountType());
|
||||
}
|
||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||
final LocalDate postInterestOnDate = null;
|
||||
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
||||
|
||||
if (backdatedTxnsAllowedTill) {
|
||||
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
} else {
|
||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
}
|
||||
|
||||
Integer accountType = null;
|
||||
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
|
||||
paymentDetail, null, accountType);
|
||||
UUID refNo = UUID.randomUUID();
|
||||
final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee(),
|
||||
backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo.toString());
|
||||
final MathContext mc = MathContext.DECIMAL64;
|
||||
|
||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||
|
||||
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
|
||||
account.postInterest(mc, today, transactionBooleanValues.isInterestTransfer(), isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
|
||||
} else {
|
||||
account.calculateInterestUsing(mc, today, transactionBooleanValues.isInterestTransfer(),
|
||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
|
||||
postReversals);
|
||||
}
|
||||
|
||||
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
|
||||
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
|
||||
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
|
||||
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
|
||||
}
|
||||
|
||||
account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(),
|
||||
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill);
|
||||
|
||||
saveTransactionToGenerateTransactionId(withdrawal);
|
||||
if (backdatedTxnsAllowedTill) {
|
||||
// Update transactions separately
|
||||
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
|
||||
}
|
||||
this.savingsAccountRepository.save(account);
|
||||
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, transactionBooleanValues.isAccountTransfer(),
|
||||
backdatedTxnsAllowedTill);
|
||||
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal));
|
||||
return withdrawal;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) {
|
||||
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
|
||||
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
|
||||
savingsAccountTransactionType, backdatedTxnsAllowedTill);
|
||||
}
|
||||
|
||||
private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||
final boolean isAccountTransfer, final boolean isRegularTransaction,
|
||||
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) {
|
||||
context.authenticatedUser();
|
||||
account.validateForAccountBlock();
|
||||
account.validateForCreditBlock();
|
||||
|
||||
// Global configurations
|
||||
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
||||
.isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
|
||||
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
|
||||
if (isRegularTransaction && !account.allowDeposit()) {
|
||||
throw new DepositAccountTransactionNotAllowedException(account.getId(), "deposit", account.depositAccountType());
|
||||
}
|
||||
boolean isInterestTransfer = false;
|
||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
||||
|
||||
if (backdatedTxnsAllowedTill) {
|
||||
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
} else {
|
||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
}
|
||||
|
||||
Integer accountType = null;
|
||||
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
|
||||
paymentDetail, null, accountType);
|
||||
UUID refNo = UUID.randomUUID();
|
||||
final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType, backdatedTxnsAllowedTill,
|
||||
relaxingDaysConfigForPivotDate, refNo.toString());
|
||||
final LocalDate postInterestOnDate = null;
|
||||
final MathContext mc = MathContext.DECIMAL64;
|
||||
|
||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||
boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
|
||||
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
|
||||
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
|
||||
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
|
||||
} else {
|
||||
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
|
||||
}
|
||||
|
||||
saveTransactionToGenerateTransactionId(deposit);
|
||||
|
||||
if (backdatedTxnsAllowedTill) {
|
||||
// Update transactions separately
|
||||
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
|
||||
}
|
||||
|
||||
this.savingsAccountRepository.saveAndFlush(account);
|
||||
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit));
|
||||
return deposit;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public SavingsAccountTransaction handleHold(final SavingsAccount account, BigDecimal amount, LocalDate transactionDate,
|
||||
Boolean lienAllowed) {
|
||||
return SavingsAccountTransaction.holdAmount(account, account.office(), null, transactionDate,
|
||||
Money.of(account.getCurrency(), amount), lienAllowed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavingsAccountTransaction handleDividendPayout(final SavingsAccount account, final LocalDate transactionDate,
|
||||
final BigDecimal transactionAmount, final boolean backdatedTxnsAllowedTill) {
|
||||
final DateTimeFormatter fmt = null;
|
||||
final PaymentDetail paymentDetail = null;
|
||||
final boolean isAccountTransfer = false;
|
||||
final boolean isRegularTransaction = true;
|
||||
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DIVIDEND_PAYOUT;
|
||||
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
|
||||
savingsAccountTransactionType, backdatedTxnsAllowedTill);
|
||||
}
|
||||
|
||||
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds,
|
||||
Set<Long> existingReversedTransactionIds) {
|
||||
existingTransactionIds.addAll(account.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds());
|
||||
}
|
||||
|
||||
private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) {
|
||||
this.savingsAccountTransactionRepository.saveAndFlush(transaction);
|
||||
return transaction.getId();
|
||||
}
|
||||
|
||||
private void saveUpdatedTransactionsOfSavingsAccount(final List<SavingsAccountTransaction> savingsAccountTransactions) {
|
||||
this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions);
|
||||
}
|
||||
|
||||
private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set<Long> existingTransactionIds,
|
||||
Set<Long> existingReversedTransactionIds) {
|
||||
existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig());
|
||||
existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig());
|
||||
}
|
||||
|
||||
private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds,
|
||||
final Set<Long> existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) {
|
||||
|
||||
final Map<String, Object> accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(),
|
||||
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
|
||||
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void postJournalEntries(final SavingsAccount account, final Set<Long> existingTransactionIds,
|
||||
final Set<Long> existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) {
|
||||
|
||||
final boolean isAccountTransfer = false;
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
|
||||
boolean backdatedTxnsAllowedTill) {
|
||||
|
||||
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
||||
.isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
|
||||
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
|
||||
final boolean postReversals = true;
|
||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
||||
|
||||
if (backdatedTxnsAllowedTill) {
|
||||
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
} else {
|
||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
}
|
||||
List<SavingsAccountTransaction> newTransactions = new ArrayList<>();
|
||||
SavingsAccountTransaction reversal = null;
|
||||
|
||||
Set<SavingsAccountChargePaidBy> chargePaidBySet = null;
|
||||
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
|
||||
reversal = SavingsAccountTransaction.reversal(savingsAccountTransaction);
|
||||
chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid();
|
||||
reversal.getSavingsAccountChargesPaid().addAll(chargePaidBySet);
|
||||
account.undoTransaction(savingsAccountTransaction);
|
||||
if (postReversals) {
|
||||
newTransactions.add(reversal);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isInterestTransfer = false;
|
||||
LocalDate postInterestOnDate = null;
|
||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||
final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode());
|
||||
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
|
||||
if (savingsAccountTransaction.isPostInterestCalculationRequired()
|
||||
&& account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill)) {
|
||||
|
||||
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
|
||||
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
|
||||
} else {
|
||||
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
|
||||
}
|
||||
account.validatePivotDateTransaction(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill,
|
||||
relaxingDaysConfigForPivotDate, "savingsaccount");
|
||||
account.validateAccountBalanceDoesNotBecomeNegativeMinimal(savingsAccountTransaction.getAmount(), false);
|
||||
account.activateAccountBasedOnBalance();
|
||||
}
|
||||
this.savingsAccountRepository.save(account);
|
||||
newTransactions.addAll(account.getSavingsAccountTransactionsWithPivotConfig());
|
||||
this.savingsAccountTransactionRepository.saveAll(newTransactions);
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, backdatedTxnsAllowedTill);
|
||||
|
||||
return reversal;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.MathContext;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsDepositBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.savings.transaction.SavingsWithdrawalBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.apache.fineract.portfolio.savings.SavingsAccountTransactionType;
|
||||
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
||||
import org.apache.fineract.portfolio.savings.data.SavingsAccountTransactionDTO;
|
||||
import org.apache.fineract.portfolio.savings.exception.DepositAccountTransactionNotAllowedException;
|
||||
import org.apache.fineract.portfolio.savings.service.SavingsAccountDomainService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class SavingsAccountDomainServiceJpa implements SavingsAccountDomainService {
|
||||
|
||||
private final PlatformSecurityContext context;
|
||||
private final SavingsAccountRepositoryWrapper savingsAccountRepository;
|
||||
private final SavingsAccountTransactionRepository savingsAccountTransactionRepository;
|
||||
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper;
|
||||
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
|
||||
private final ConfigurationDomainService configurationDomainService;
|
||||
private final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
|
||||
@Autowired
|
||||
public SavingsAccountDomainServiceJpa(final SavingsAccountRepositoryWrapper savingsAccountRepository,
|
||||
final SavingsAccountTransactionRepository savingsAccountTransactionRepository,
|
||||
final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepositoryWrapper,
|
||||
final JournalEntryWritePlatformService journalEntryWritePlatformService,
|
||||
final ConfigurationDomainService configurationDomainService, final PlatformSecurityContext context,
|
||||
final DepositAccountOnHoldTransactionRepository depositAccountOnHoldTransactionRepository,
|
||||
final BusinessEventNotifierService businessEventNotifierService) {
|
||||
this.savingsAccountRepository = savingsAccountRepository;
|
||||
this.savingsAccountTransactionRepository = savingsAccountTransactionRepository;
|
||||
this.applicationCurrencyRepositoryWrapper = applicationCurrencyRepositoryWrapper;
|
||||
this.journalEntryWritePlatformService = journalEntryWritePlatformService;
|
||||
this.configurationDomainService = configurationDomainService;
|
||||
this.context = context;
|
||||
this.depositAccountOnHoldTransactionRepository = depositAccountOnHoldTransactionRepository;
|
||||
this.businessEventNotifierService = businessEventNotifierService;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public SavingsAccountTransaction handleWithdrawal(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||
final SavingsTransactionBooleanValues transactionBooleanValues, final boolean backdatedTxnsAllowedTill) {
|
||||
context.authenticatedUser();
|
||||
account.validateForAccountBlock();
|
||||
account.validateForDebitBlock();
|
||||
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
||||
.isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
|
||||
final boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
|
||||
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
|
||||
if (transactionBooleanValues.isRegularTransaction() && !account.allowWithdrawal()) {
|
||||
throw new DepositAccountTransactionNotAllowedException(account.getId(), "withdraw", account.depositAccountType());
|
||||
}
|
||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||
final LocalDate postInterestOnDate = null;
|
||||
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
||||
|
||||
if (backdatedTxnsAllowedTill) {
|
||||
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
} else {
|
||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
}
|
||||
|
||||
Integer accountType = null;
|
||||
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
|
||||
paymentDetail, null, accountType);
|
||||
UUID refNo = UUID.randomUUID();
|
||||
final SavingsAccountTransaction withdrawal = account.withdraw(transactionDTO, transactionBooleanValues.isApplyWithdrawFee(),
|
||||
backdatedTxnsAllowedTill, relaxingDaysConfigForPivotDate, refNo.toString());
|
||||
final MathContext mc = MathContext.DECIMAL64;
|
||||
|
||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||
|
||||
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
|
||||
account.postInterest(mc, today, transactionBooleanValues.isInterestTransfer(), isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
|
||||
} else {
|
||||
account.calculateInterestUsing(mc, today, transactionBooleanValues.isInterestTransfer(),
|
||||
isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill,
|
||||
postReversals);
|
||||
}
|
||||
|
||||
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
|
||||
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
|
||||
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
|
||||
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
|
||||
}
|
||||
|
||||
account.validateAccountBalanceDoesNotBecomeNegative(transactionAmount, transactionBooleanValues.isExceptionForBalanceCheck(),
|
||||
depositAccountOnHoldTransactions, backdatedTxnsAllowedTill);
|
||||
|
||||
saveTransactionToGenerateTransactionId(withdrawal);
|
||||
if (backdatedTxnsAllowedTill) {
|
||||
// Update transactions separately
|
||||
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
|
||||
}
|
||||
this.savingsAccountRepository.save(account);
|
||||
|
||||
if (!transactionBooleanValues.isSkipAccountingPostings()) {
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds,
|
||||
transactionBooleanValues.isAccountTransfer(), backdatedTxnsAllowedTill);
|
||||
}
|
||||
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new SavingsWithdrawalBusinessEvent(withdrawal));
|
||||
return withdrawal;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill) {
|
||||
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
|
||||
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
|
||||
savingsAccountTransactionType, backdatedTxnsAllowedTill, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||
final boolean isAccountTransfer, final boolean isRegularTransaction, final boolean backdatedTxnsAllowedTill,
|
||||
final boolean skipAccountingPostings) {
|
||||
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DEPOSIT;
|
||||
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
|
||||
savingsAccountTransactionType, backdatedTxnsAllowedTill, skipAccountingPostings);
|
||||
}
|
||||
|
||||
private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||
final boolean isAccountTransfer, final boolean isRegularTransaction,
|
||||
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill) {
|
||||
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
|
||||
savingsAccountTransactionType, backdatedTxnsAllowedTill, false);
|
||||
}
|
||||
|
||||
private SavingsAccountTransaction handleDeposit(final SavingsAccount account, final DateTimeFormatter fmt,
|
||||
final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail,
|
||||
final boolean isAccountTransfer, final boolean isRegularTransaction,
|
||||
final SavingsAccountTransactionType savingsAccountTransactionType, final boolean backdatedTxnsAllowedTill,
|
||||
final boolean skipAccountingPostings) {
|
||||
context.authenticatedUser();
|
||||
account.validateForAccountBlock();
|
||||
account.validateForCreditBlock();
|
||||
|
||||
// Global configurations
|
||||
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
||||
.isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
|
||||
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
|
||||
if (isRegularTransaction && !account.allowDeposit()) {
|
||||
throw new DepositAccountTransactionNotAllowedException(account.getId(), "deposit", account.depositAccountType());
|
||||
}
|
||||
boolean isInterestTransfer = false;
|
||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
||||
|
||||
if (backdatedTxnsAllowedTill) {
|
||||
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
} else {
|
||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
}
|
||||
|
||||
Integer accountType = null;
|
||||
final SavingsAccountTransactionDTO transactionDTO = new SavingsAccountTransactionDTO(fmt, transactionDate, transactionAmount,
|
||||
paymentDetail, null, accountType);
|
||||
UUID refNo = UUID.randomUUID();
|
||||
final SavingsAccountTransaction deposit = account.deposit(transactionDTO, savingsAccountTransactionType, backdatedTxnsAllowedTill,
|
||||
relaxingDaysConfigForPivotDate, refNo.toString());
|
||||
final LocalDate postInterestOnDate = null;
|
||||
final MathContext mc = MathContext.DECIMAL64;
|
||||
|
||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||
boolean postReversals = this.configurationDomainService.isReversalTransactionAllowed();
|
||||
if (account.isBeforeLastPostingPeriod(transactionDate, backdatedTxnsAllowedTill)) {
|
||||
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
|
||||
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
|
||||
} else {
|
||||
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
|
||||
}
|
||||
|
||||
saveTransactionToGenerateTransactionId(deposit);
|
||||
|
||||
if (backdatedTxnsAllowedTill) {
|
||||
// Update transactions separately
|
||||
saveUpdatedTransactionsOfSavingsAccount(account.getSavingsAccountTransactionsWithPivotConfig());
|
||||
}
|
||||
|
||||
this.savingsAccountRepository.saveAndFlush(account);
|
||||
|
||||
if (!skipAccountingPostings) {
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
|
||||
}
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new SavingsDepositBusinessEvent(deposit));
|
||||
return deposit;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public SavingsAccountTransaction handleHold(final SavingsAccount account, BigDecimal amount, LocalDate transactionDate,
|
||||
Boolean lienAllowed) {
|
||||
return SavingsAccountTransaction.holdAmount(account, account.office(), null, transactionDate,
|
||||
Money.of(account.getCurrency(), amount), lienAllowed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavingsAccountTransaction handleDividendPayout(final SavingsAccount account, final LocalDate transactionDate,
|
||||
final BigDecimal transactionAmount, final boolean backdatedTxnsAllowedTill) {
|
||||
final DateTimeFormatter fmt = null;
|
||||
final PaymentDetail paymentDetail = null;
|
||||
final boolean isAccountTransfer = false;
|
||||
final boolean isRegularTransaction = true;
|
||||
final SavingsAccountTransactionType savingsAccountTransactionType = SavingsAccountTransactionType.DIVIDEND_PAYOUT;
|
||||
return handleDeposit(account, fmt, transactionDate, transactionAmount, paymentDetail, isAccountTransfer, isRegularTransaction,
|
||||
savingsAccountTransactionType, backdatedTxnsAllowedTill);
|
||||
}
|
||||
|
||||
private void updateExistingTransactionsDetails(SavingsAccount account, Set<Long> existingTransactionIds,
|
||||
Set<Long> existingReversedTransactionIds) {
|
||||
existingTransactionIds.addAll(account.findExistingTransactionIds());
|
||||
existingReversedTransactionIds.addAll(account.findExistingReversedTransactionIds());
|
||||
}
|
||||
|
||||
private Long saveTransactionToGenerateTransactionId(final SavingsAccountTransaction transaction) {
|
||||
this.savingsAccountTransactionRepository.saveAndFlush(transaction);
|
||||
return transaction.getId();
|
||||
}
|
||||
|
||||
private void saveUpdatedTransactionsOfSavingsAccount(final List<SavingsAccountTransaction> savingsAccountTransactions) {
|
||||
this.savingsAccountTransactionRepository.saveAll(savingsAccountTransactions);
|
||||
}
|
||||
|
||||
private void updateTransactionDetailsWithPivotConfig(final SavingsAccount account, Set<Long> existingTransactionIds,
|
||||
Set<Long> existingReversedTransactionIds) {
|
||||
existingTransactionIds.addAll(account.findCurrentTransactionIdsWithPivotDateConfig());
|
||||
existingReversedTransactionIds.addAll(account.findCurrentReversedTransactionIdsWithPivotDateConfig());
|
||||
}
|
||||
|
||||
private void postJournalEntries(final SavingsAccount savingsAccount, final Set<Long> existingTransactionIds,
|
||||
final Set<Long> existingReversedTransactionIds, boolean isAccountTransfer, final boolean backdatedTxnsAllowedTill) {
|
||||
|
||||
final Map<String, Object> accountingBridgeData = savingsAccount.deriveAccountingBridgeData(savingsAccount.getCurrency().getCode(),
|
||||
existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
|
||||
this.journalEntryWritePlatformService.createJournalEntriesForSavings(accountingBridgeData);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void postJournalEntries(final SavingsAccount account, final Set<Long> existingTransactionIds,
|
||||
final Set<Long> existingReversedTransactionIds, final boolean backdatedTxnsAllowedTill) {
|
||||
|
||||
final boolean isAccountTransfer = false;
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, isAccountTransfer, backdatedTxnsAllowedTill);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
|
||||
boolean backdatedTxnsAllowedTill) {
|
||||
|
||||
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
|
||||
.isSavingsInterestPostingAtCurrentPeriodEnd();
|
||||
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
|
||||
final Long relaxingDaysConfigForPivotDate = this.configurationDomainService.retrieveRelaxingDaysConfigForPivotDate();
|
||||
final boolean postReversals = true;
|
||||
final Set<Long> existingTransactionIds = new HashSet<>();
|
||||
final Set<Long> existingReversedTransactionIds = new HashSet<>();
|
||||
|
||||
if (backdatedTxnsAllowedTill) {
|
||||
updateTransactionDetailsWithPivotConfig(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
} else {
|
||||
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
|
||||
}
|
||||
List<SavingsAccountTransaction> newTransactions = new ArrayList<>();
|
||||
SavingsAccountTransaction reversal = null;
|
||||
|
||||
Set<SavingsAccountChargePaidBy> chargePaidBySet = null;
|
||||
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
|
||||
reversal = SavingsAccountTransaction.reversal(savingsAccountTransaction);
|
||||
chargePaidBySet = savingsAccountTransaction.getSavingsAccountChargesPaid();
|
||||
reversal.getSavingsAccountChargesPaid().addAll(chargePaidBySet);
|
||||
account.undoTransaction(savingsAccountTransaction);
|
||||
if (postReversals) {
|
||||
newTransactions.add(reversal);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isInterestTransfer = false;
|
||||
LocalDate postInterestOnDate = null;
|
||||
final LocalDate today = DateUtils.getBusinessLocalDate();
|
||||
final MathContext mc = new MathContext(15, MoneyHelper.getRoundingMode());
|
||||
for (SavingsAccountTransaction savingsAccountTransaction : savingsAccountTransactions) {
|
||||
if (savingsAccountTransaction.isPostInterestCalculationRequired()
|
||||
&& account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill)) {
|
||||
|
||||
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
|
||||
postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
|
||||
} else {
|
||||
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
|
||||
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
|
||||
}
|
||||
account.validatePivotDateTransaction(savingsAccountTransaction.getTransactionDate(), backdatedTxnsAllowedTill,
|
||||
relaxingDaysConfigForPivotDate, "savingsaccount");
|
||||
account.validateAccountBalanceDoesNotBecomeNegativeMinimal(savingsAccountTransaction.getAmount(), false);
|
||||
account.activateAccountBasedOnBalance();
|
||||
}
|
||||
this.savingsAccountRepository.save(account);
|
||||
newTransactions.addAll(account.getSavingsAccountTransactionsWithPivotConfig());
|
||||
this.savingsAccountTransactionRepository.saveAll(newTransactions);
|
||||
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds, false, backdatedTxnsAllowedTill);
|
||||
|
||||
return reversal;
|
||||
}
|
||||
}
|
||||
|
||||
+70
-59
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+55
-51
@@ -1,51 +1,55 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
|
||||
|
||||
public interface SavingsAccountDomainService {
|
||||
|
||||
SavingsAccountTransaction handleWithdrawal(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues,
|
||||
boolean backdatedTxnsAllowedTill);
|
||||
|
||||
SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
|
||||
boolean backdatedTxnsAllowedTill);
|
||||
|
||||
void postJournalEntries(SavingsAccount savingsAccount, Set<Long> existingTransactionIds, Set<Long> existingReversedTransactionIds,
|
||||
boolean backdatedTxnsAllowedTill);
|
||||
|
||||
SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount,
|
||||
boolean backdatedTxnsAllowedTill);
|
||||
|
||||
SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
|
||||
boolean backdatedTxnsAllowedTill);
|
||||
|
||||
SavingsAccountTransaction handleHold(SavingsAccount account, BigDecimal amount, LocalDate transactionDate, Boolean lienAllowed);
|
||||
}
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
package org.apache.fineract.portfolio.savings.service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||
import org.apache.fineract.portfolio.savings.SavingsTransactionBooleanValues;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccount;
|
||||
import org.apache.fineract.portfolio.savings.domain.SavingsAccountTransaction;
|
||||
|
||||
public interface SavingsAccountDomainService {
|
||||
|
||||
SavingsAccountTransaction handleWithdrawal(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, SavingsTransactionBooleanValues transactionBooleanValues,
|
||||
boolean backdatedTxnsAllowedTill);
|
||||
|
||||
SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
|
||||
boolean backdatedTxnsAllowedTill);
|
||||
|
||||
SavingsAccountTransaction handleDeposit(SavingsAccount account, DateTimeFormatter fmt, LocalDate transactionDate,
|
||||
BigDecimal transactionAmount, PaymentDetail paymentDetail, boolean isAccountTransfer, boolean isRegularTransaction,
|
||||
boolean backdatedTxnsAllowedTill, boolean skipAccountingPostings);
|
||||
|
||||
void postJournalEntries(SavingsAccount savingsAccount, Set<Long> existingTransactionIds, Set<Long> existingReversedTransactionIds,
|
||||
boolean backdatedTxnsAllowedTill);
|
||||
|
||||
SavingsAccountTransaction handleDividendPayout(SavingsAccount account, LocalDate transactionDate, BigDecimal transactionAmount,
|
||||
boolean backdatedTxnsAllowedTill);
|
||||
|
||||
SavingsAccountTransaction handleReversal(SavingsAccount account, List<SavingsAccountTransaction> savingsAccountTransactions,
|
||||
boolean backdatedTxnsAllowedTill);
|
||||
|
||||
SavingsAccountTransaction handleHold(SavingsAccount account, BigDecimal amount, LocalDate transactionDate, Boolean lienAllowed);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user