blob: fa1d7273748cba40c85e51cc877ec89e75174869 [file] [log] [blame]
/**
* 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.accounting.journalentry.service;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.accounting.closure.domain.GLClosure;
import org.apache.fineract.accounting.closure.domain.GLClosureRepository;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
import org.apache.fineract.accounting.glaccount.data.GLAccountDataForLookup;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.glaccount.domain.GLAccountRepository;
import org.apache.fineract.accounting.glaccount.domain.GLAccountType;
import org.apache.fineract.accounting.glaccount.exception.GLAccountNotFoundException;
import org.apache.fineract.accounting.glaccount.service.GLAccountReadPlatformService;
import org.apache.fineract.accounting.journalentry.api.JournalEntryJsonInputParams;
import org.apache.fineract.accounting.journalentry.command.JournalEntryCommand;
import org.apache.fineract.accounting.journalentry.command.SingleDebitOrCreditEntryCommand;
import org.apache.fineract.accounting.journalentry.data.ClientTransactionDTO;
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
import org.apache.fineract.accounting.journalentry.data.SavingsDTO;
import org.apache.fineract.accounting.journalentry.data.SharesDTO;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
import org.apache.fineract.accounting.journalentry.exception.JournalEntriesNotFoundException;
import org.apache.fineract.accounting.journalentry.exception.JournalEntryInvalidException;
import org.apache.fineract.accounting.journalentry.exception.JournalEntryInvalidException.GlJournalEntryInvalidReason;
import org.apache.fineract.accounting.journalentry.exception.JournalEntryRuntimeException;
import org.apache.fineract.accounting.journalentry.serialization.JournalEntryCommandFromApiJsonDeserializer;
import org.apache.fineract.accounting.producttoaccountmapping.domain.PortfolioProductType;
import org.apache.fineract.accounting.provisioning.domain.LoanProductProvisioningEntry;
import org.apache.fineract.accounting.provisioning.domain.ProvisioningEntry;
import org.apache.fineract.accounting.rule.domain.AccountingRule;
import org.apache.fineract.accounting.rule.domain.AccountingRuleRepository;
import org.apache.fineract.accounting.rule.exception.AccountingRuleNotFoundException;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
import org.apache.fineract.organisation.office.domain.OrganisationCurrencyRepositoryWrapper;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService;
import org.apache.fineract.useradministration.domain.AppUser;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
@RequiredArgsConstructor
@Slf4j
public class JournalEntryWritePlatformServiceJpaRepositoryImpl implements JournalEntryWritePlatformService {
private final GLClosureRepository glClosureRepository;
private final GLAccountRepository glAccountRepository;
private final JournalEntryRepository glJournalEntryRepository;
private final OfficeRepositoryWrapper officeRepositoryWrapper;
private final AccountingProcessorForLoanFactory accountingProcessorForLoanFactory;
private final AccountingProcessorForSavingsFactory accountingProcessorForSavingsFactory;
private final AccountingProcessorForSharesFactory accountingProcessorForSharesFactory;
private final AccountingProcessorHelper helper;
private final JournalEntryCommandFromApiJsonDeserializer fromApiJsonDeserializer;
private final AccountingRuleRepository accountingRuleRepository;
private final GLAccountReadPlatformService glAccountReadPlatformService;
private final OrganisationCurrencyRepositoryWrapper organisationCurrencyRepository;
private final PlatformSecurityContext context;
private final PaymentDetailWritePlatformService paymentDetailWritePlatformService;
private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepositoryWrapper;
private final CashBasedAccountingProcessorForClientTransactions accountingProcessorForClientTransactions;
@Transactional
@Override
public CommandProcessingResult createJournalEntry(final JsonCommand command) {
try {
final JournalEntryCommand journalEntryCommand = this.fromApiJsonDeserializer.commandFromApiJson(command.json());
journalEntryCommand.validateForCreate();
// check office is valid
final Long officeId = command.longValueOfParameterNamed(JournalEntryJsonInputParams.OFFICE_ID.getValue());
final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final Long accountRuleId = command.longValueOfParameterNamed(JournalEntryJsonInputParams.ACCOUNTING_RULE.getValue());
final String currencyCode = command.stringValueOfParameterNamed(JournalEntryJsonInputParams.CURRENCY_CODE.getValue());
validateBusinessRulesForJournalEntries(journalEntryCommand);
/** Capture payment details **/
final Map<String, Object> changes = new LinkedHashMap<>();
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
/** Set a transaction Id and save these Journal entries **/
final LocalDate transactionDate = command
.localDateValueOfParameterNamed(JournalEntryJsonInputParams.TRANSACTION_DATE.getValue());
final String transactionId = generateTransactionId(officeId);
final String referenceNumber = command.stringValueOfParameterNamed(JournalEntryJsonInputParams.REFERENCE_NUMBER.getValue());
if (accountRuleId != null) {
final AccountingRule accountingRule = this.accountingRuleRepository.findById(accountRuleId)
.orElseThrow(() -> new AccountingRuleNotFoundException(accountRuleId));
if (accountingRule.getAccountToCredit() == null) {
if (journalEntryCommand.getCredits() == null) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.NO_DEBITS_OR_CREDITS, null, null, null);
}
if (journalEntryCommand.getDebits() != null) {
checkDebitOrCreditAccountsAreValid(accountingRule, journalEntryCommand.getCredits(),
journalEntryCommand.getDebits());
checkDebitAndCreditAmounts(journalEntryCommand.getCredits(), journalEntryCommand.getDebits());
}
saveAllDebitOrCreditEntries(journalEntryCommand, office, paymentDetail, currencyCode, transactionDate,
journalEntryCommand.getCredits(), transactionId, JournalEntryType.CREDIT, referenceNumber);
} else {
final GLAccount creditAccountHead = accountingRule.getAccountToCredit();
validateGLAccountForTransaction(creditAccountHead);
validateDebitOrCreditArrayForExistingGLAccount(creditAccountHead, journalEntryCommand.getCredits());
saveAllDebitOrCreditEntries(journalEntryCommand, office, paymentDetail, currencyCode, transactionDate,
journalEntryCommand.getCredits(), transactionId, JournalEntryType.CREDIT, referenceNumber);
}
if (accountingRule.getAccountToDebit() == null) {
if (journalEntryCommand.getDebits() == null) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.NO_DEBITS_OR_CREDITS, null, null, null);
}
if (journalEntryCommand.getCredits() != null) {
checkDebitOrCreditAccountsAreValid(accountingRule, journalEntryCommand.getCredits(),
journalEntryCommand.getDebits());
checkDebitAndCreditAmounts(journalEntryCommand.getCredits(), journalEntryCommand.getDebits());
}
saveAllDebitOrCreditEntries(journalEntryCommand, office, paymentDetail, currencyCode, transactionDate,
journalEntryCommand.getDebits(), transactionId, JournalEntryType.DEBIT, referenceNumber);
} else {
final GLAccount debitAccountHead = accountingRule.getAccountToDebit();
validateGLAccountForTransaction(debitAccountHead);
validateDebitOrCreditArrayForExistingGLAccount(debitAccountHead, journalEntryCommand.getDebits());
saveAllDebitOrCreditEntries(journalEntryCommand, office, paymentDetail, currencyCode, transactionDate,
journalEntryCommand.getDebits(), transactionId, JournalEntryType.DEBIT, referenceNumber);
}
} else {
saveAllDebitOrCreditEntries(journalEntryCommand, office, paymentDetail, currencyCode, transactionDate,
journalEntryCommand.getDebits(), transactionId, JournalEntryType.DEBIT, referenceNumber);
saveAllDebitOrCreditEntries(journalEntryCommand, office, paymentDetail, currencyCode, transactionDate,
journalEntryCommand.getCredits(), transactionId, JournalEntryType.CREDIT, referenceNumber);
}
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withOfficeId(officeId)
.withTransactionId(transactionId).build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
throw handleJournalEntryDataIntegrityIssues(throwable, dve);
}
}
private void validateDebitOrCreditArrayForExistingGLAccount(final GLAccount glaccount,
final SingleDebitOrCreditEntryCommand[] creditOrDebits) {
/**
* If a glaccount is assigned for a rule the credits or debits array should have only one entry and it must be
* same as existing account
*/
if (creditOrDebits.length != 1) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.INVALID_DEBIT_OR_CREDIT_ACCOUNTS, null, null, null);
}
for (final SingleDebitOrCreditEntryCommand creditOrDebit : creditOrDebits) {
if (glaccount == null || creditOrDebit == null || !Objects.equals(glaccount.getId(), creditOrDebit.getGlAccountId())) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.INVALID_DEBIT_OR_CREDIT_ACCOUNTS, null, null, null);
}
}
}
@SuppressWarnings("null")
private void checkDebitOrCreditAccountsAreValid(final AccountingRule accountingRule, final SingleDebitOrCreditEntryCommand[] credits,
final SingleDebitOrCreditEntryCommand[] debits) {
// Validate the debit and credit arrays are appropriate accounts
List<GLAccountDataForLookup> allowedCreditGLAccounts;
List<GLAccountDataForLookup> allowedDebitGLAccounts;
int validCreditsNo = 0;
int validDebitsNo = 0;
if (credits != null && credits.length > 0) {
allowedCreditGLAccounts = this.glAccountReadPlatformService.retrieveAccountsByTagId(accountingRule.getId(),
JournalEntryType.CREDIT.getValue());
for (final GLAccountDataForLookup accountDataForLookup : allowedCreditGLAccounts) {
for (final SingleDebitOrCreditEntryCommand credit : credits) {
if (credit.getGlAccountId().equals(accountDataForLookup.getId())) {
validCreditsNo++;
}
}
}
if (credits.length != validCreditsNo) {
throw new JournalEntryRuntimeException("error.msg.glJournalEntry.invalid.credits", "Invalid Credits.");
}
}
if (debits != null && debits.length > 0) {
allowedDebitGLAccounts = this.glAccountReadPlatformService.retrieveAccountsByTagId(accountingRule.getId(),
JournalEntryType.DEBIT.getValue());
for (final GLAccountDataForLookup accountDataForLookup : allowedDebitGLAccounts) {
for (final SingleDebitOrCreditEntryCommand debit : debits) {
if (debit.getGlAccountId().equals(accountDataForLookup.getId())) {
validDebitsNo++;
}
}
}
if (debits.length != validDebitsNo) {
throw new JournalEntryRuntimeException("error.msg.glJournalEntry.invalid.debits", "Invalid Debits");
}
}
}
private void checkDebitAndCreditAmounts(final SingleDebitOrCreditEntryCommand[] credits,
final SingleDebitOrCreditEntryCommand[] debits) {
// sum of all debits must be = sum of all credits
BigDecimal creditsSum = BigDecimal.ZERO;
BigDecimal debitsSum = BigDecimal.ZERO;
for (final SingleDebitOrCreditEntryCommand creditEntryCommand : credits) {
if (creditEntryCommand.getAmount() == null || creditEntryCommand.getGlAccountId() == null) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null);
}
creditsSum = creditsSum.add(creditEntryCommand.getAmount());
}
for (final SingleDebitOrCreditEntryCommand debitEntryCommand : debits) {
if (debitEntryCommand.getAmount() == null || debitEntryCommand.getGlAccountId() == null) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.DEBIT_CREDIT_ACCOUNT_OR_AMOUNT_EMPTY, null, null, null);
}
debitsSum = debitsSum.add(debitEntryCommand.getAmount());
}
if (creditsSum.compareTo(debitsSum) != 0) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.DEBIT_CREDIT_SUM_MISMATCH, null, null, null);
}
}
private void validateGLAccountForTransaction(final GLAccount creditOrDebitAccountHead) {
/***
* validate that the account allows manual adjustments and is not disabled
**/
if (creditOrDebitAccountHead.isDisabled()) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.GL_ACCOUNT_DISABLED, null,
creditOrDebitAccountHead.getName(), creditOrDebitAccountHead.getGlCode());
} else if (!creditOrDebitAccountHead.isManualEntriesAllowed()) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.GL_ACCOUNT_MANUAL_ENTRIES_NOT_PERMITTED, null,
creditOrDebitAccountHead.getName(), creditOrDebitAccountHead.getGlCode());
}
}
@Transactional
@Override
public CommandProcessingResult revertJournalEntry(final JsonCommand command) {
// is the transaction Id valid
final List<JournalEntry> journalEntries = this.glJournalEntryRepository
.findUnReversedManualJournalEntriesByTransactionId(command.getTransactionId());
String reversalComment = command.stringValueOfParameterNamed("comments");
if (journalEntries.size() <= 1) {
throw new JournalEntriesNotFoundException(command.getTransactionId());
}
final String reversalTransactionId = revertJournalEntry(journalEntries, reversalComment);
return new CommandProcessingResultBuilder().withTransactionId(reversalTransactionId).build();
}
public String revertJournalEntry(final List<JournalEntry> journalEntries, String reversalComment) {
final Long officeId = journalEntries.get(0).getOffice().getId();
final String reversalTransactionId = generateTransactionId(officeId);
final boolean manualEntry = true;
final boolean useDefaultComment = StringUtils.isBlank(reversalComment);
validateCommentForReversal(reversalComment);
// Before reversal validate accounting closure is done for that branch
// or not.
final LocalDate journalEntriesTransactionDate = journalEntries.get(0).getTransactionDate();
final GLClosure latestGLClosureByBranch = this.glClosureRepository.getLatestGLClosureByBranch(officeId);
if (latestGLClosureByBranch != null) {
if (!DateUtils.isBefore(latestGLClosureByBranch.getClosingDate(), journalEntriesTransactionDate)) {
final String accountName = null;
final String accountGLCode = null;
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.ACCOUNTING_CLOSED,
latestGLClosureByBranch.getClosingDate(), accountName, accountGLCode);
}
}
for (final JournalEntry journalEntry : journalEntries) {
JournalEntry reversalJournalEntry;
if (useDefaultComment) {
reversalComment = "Reversal entry for Journal Entry with Entry Id :" + journalEntry.getId() + " and transaction Id "
+ journalEntry.getTransactionId();
}
if (journalEntry.isDebitEntry()) {
reversalJournalEntry = JournalEntry.createNew(journalEntry.getOffice(), journalEntry.getPaymentDetail(),
journalEntry.getGlAccount(), journalEntry.getCurrencyCode(), reversalTransactionId, manualEntry,
journalEntry.getTransactionDate(), JournalEntryType.CREDIT, journalEntry.getAmount(), reversalComment, null, null,
journalEntry.getReferenceNumber(), journalEntry.getLoanTransactionId(), journalEntry.getSavingsTransactionId(),
journalEntry.getClientTransactionId(), journalEntry.getShareTransactionId());
} else {
reversalJournalEntry = JournalEntry.createNew(journalEntry.getOffice(), journalEntry.getPaymentDetail(),
journalEntry.getGlAccount(), journalEntry.getCurrencyCode(), reversalTransactionId, manualEntry,
journalEntry.getTransactionDate(), JournalEntryType.DEBIT, journalEntry.getAmount(), reversalComment, null, null,
journalEntry.getReferenceNumber(), journalEntry.getLoanTransactionId(), journalEntry.getSavingsTransactionId(),
journalEntry.getClientTransactionId(), journalEntry.getShareTransactionId());
}
// save the reversal entry
helper.persistJournalEntry(reversalJournalEntry);
journalEntry.setReversed(true);
journalEntry.setReversalJournalEntry(reversalJournalEntry);
// save the updated journal entry
helper.persistJournalEntry(journalEntry);
}
return reversalTransactionId;
}
@Override
public String revertProvisioningJournalEntries(final LocalDate reversalTransactionDate, final Long entityId, final Integer entityType) {
List<JournalEntry> journalEntries = this.glJournalEntryRepository.findProvisioningJournalEntriesByEntityId(entityId, entityType);
final String reversalTransactionId = journalEntries.get(0).getTransactionId();
for (final JournalEntry journalEntry : journalEntries) {
JournalEntry reversalJournalEntry;
String reversalComment = "Reversal entry for Journal Entry with Entry Id :" + journalEntry.getId() + " and transaction Id "
+ journalEntry.getTransactionId();
if (journalEntry.isDebitEntry()) {
reversalJournalEntry = JournalEntry.createNew(journalEntry.getOffice(), journalEntry.getPaymentDetail(),
journalEntry.getGlAccount(), journalEntry.getCurrencyCode(), journalEntry.getTransactionId(), Boolean.FALSE,
reversalTransactionDate, JournalEntryType.CREDIT, journalEntry.getAmount(), reversalComment,
journalEntry.getEntityType(), journalEntry.getEntityId(), journalEntry.getReferenceNumber(),
journalEntry.getLoanTransactionId(), journalEntry.getSavingsTransactionId(), journalEntry.getClientTransactionId(),
journalEntry.getShareTransactionId());
} else {
reversalJournalEntry = JournalEntry.createNew(journalEntry.getOffice(), journalEntry.getPaymentDetail(),
journalEntry.getGlAccount(), journalEntry.getCurrencyCode(), journalEntry.getTransactionId(), Boolean.FALSE,
reversalTransactionDate, JournalEntryType.DEBIT, journalEntry.getAmount(), reversalComment,
journalEntry.getEntityType(), journalEntry.getEntityId(), journalEntry.getReferenceNumber(),
journalEntry.getLoanTransactionId(), journalEntry.getSavingsTransactionId(), journalEntry.getClientTransactionId(),
journalEntry.getShareTransactionId());
}
// save the reversal entry
helper.persistJournalEntry(reversalJournalEntry);
journalEntry.setReversalJournalEntry(reversalJournalEntry);
journalEntry.setReversed(true);
// save the updated journal entry
helper.persistJournalEntry(journalEntry);
}
return reversalTransactionId;
}
@Override
public String createProvisioningJournalEntries(ProvisioningEntry provisioningEntry) {
Collection<LoanProductProvisioningEntry> provisioningEntries = provisioningEntry.getLoanProductProvisioningEntries();
Map<OfficeCurrencyKey, List<LoanProductProvisioningEntry>> officeMap = new HashMap<>();
for (LoanProductProvisioningEntry entry : provisioningEntries) {
OfficeCurrencyKey key = new OfficeCurrencyKey(entry.getOffice(), entry.getCurrencyCode());
if (officeMap.containsKey(key)) {
List<LoanProductProvisioningEntry> list = officeMap.get(key);
list.add(entry);
} else {
List<LoanProductProvisioningEntry> list = new ArrayList<>();
list.add(entry);
officeMap.put(key, list);
}
}
Map<GLAccount, BigDecimal> liabilityMap = new HashMap<>();
Map<GLAccount, BigDecimal> expenseMap = new HashMap<>();
for (Map.Entry<OfficeCurrencyKey, List<LoanProductProvisioningEntry>> entry : officeMap.entrySet()) {
liabilityMap.clear();
expenseMap.clear();
for (LoanProductProvisioningEntry lppEntry : entry.getValue()) {
if (liabilityMap.containsKey(lppEntry.getLiabilityAccount())) {
BigDecimal amount = liabilityMap.get(lppEntry.getLiabilityAccount());
amount = amount.add(lppEntry.getReservedAmount());
liabilityMap.put(lppEntry.getLiabilityAccount(), amount);
} else {
BigDecimal amount = BigDecimal.ZERO.add(lppEntry.getReservedAmount());
liabilityMap.put(lppEntry.getLiabilityAccount(), amount);
}
if (expenseMap.containsKey(lppEntry.getExpenseAccount())) {
BigDecimal amount = expenseMap.get(lppEntry.getExpenseAccount());
amount = amount.add(lppEntry.getReservedAmount());
expenseMap.put(lppEntry.getExpenseAccount(), amount);
} else {
BigDecimal amount = BigDecimal.ZERO.add(lppEntry.getReservedAmount());
expenseMap.put(lppEntry.getExpenseAccount(), amount);
}
}
createJournalEntry(provisioningEntry.getCreatedDate(), provisioningEntry.getId(), entry.getKey().office,
entry.getKey().currency, liabilityMap, expenseMap);
}
return "P" + provisioningEntry.getId();
}
private void createJournalEntry(LocalDate transactionDate, Long entryId, Office office, String currencyCode,
Map<GLAccount, BigDecimal> liabilityMap, Map<GLAccount, BigDecimal> expenseMap) {
for (Map.Entry<GLAccount, BigDecimal> entry : liabilityMap.entrySet()) {
this.helper.createProvisioningCreditJournalEntry(transactionDate, entryId, office, currencyCode, entry.getKey(),
entry.getValue());
}
for (Map.Entry<GLAccount, BigDecimal> entry : expenseMap.entrySet()) {
this.helper.createProvisioningDebitJournalEntry(transactionDate, entryId, office, currencyCode, entry.getKey(),
entry.getValue());
}
}
private void validateCommentForReversal(final String reversalComment) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("GLJournalEntry");
baseDataValidator.reset().parameter("comments").value(reversalComment).notExceedingLengthOf(500);
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.",
dataValidationErrors);
}
}
@Transactional
@Override
public void createJournalEntriesForLoan(final Map<String, Object> accountingBridgeData) {
final boolean cashBasedAccountingEnabled = (Boolean) accountingBridgeData.get("cashBasedAccountingEnabled");
final boolean upfrontAccrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("upfrontAccrualBasedAccountingEnabled");
final boolean periodicAccrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("periodicAccrualBasedAccountingEnabled");
if (cashBasedAccountingEnabled || upfrontAccrualBasedAccountingEnabled || periodicAccrualBasedAccountingEnabled) {
final LoanDTO loanDTO = this.helper.populateLoanDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled,
upfrontAccrualBasedAccountingEnabled, periodicAccrualBasedAccountingEnabled);
final AccountingProcessorForLoan accountingProcessorForLoan = this.accountingProcessorForLoanFactory
.determineProcessor(loanDTO);
accountingProcessorForLoan.createJournalEntriesForLoan(loanDTO);
}
}
@Transactional
@Override
public void createJournalEntriesForSavings(final Map<String, Object> accountingBridgeData) {
final boolean cashBasedAccountingEnabled = (Boolean) accountingBridgeData.get("cashBasedAccountingEnabled");
final boolean accrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("accrualBasedAccountingEnabled");
if (cashBasedAccountingEnabled || accrualBasedAccountingEnabled) {
final SavingsDTO savingsDTO = this.helper.populateSavingsDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled,
accrualBasedAccountingEnabled);
final AccountingProcessorForSavings accountingProcessorForSavings = this.accountingProcessorForSavingsFactory
.determineProcessor(savingsDTO);
accountingProcessorForSavings.createJournalEntriesForSavings(savingsDTO);
}
}
@Transactional
@Override
public void createJournalEntriesForShares(final Map<String, Object> accountingBridgeData) {
final boolean cashBasedAccountingEnabled = (Boolean) accountingBridgeData.get("cashBasedAccountingEnabled");
final boolean accrualBasedAccountingEnabled = (Boolean) accountingBridgeData.get("accrualBasedAccountingEnabled");
if (cashBasedAccountingEnabled) {
final SharesDTO sharesDTO = this.helper.populateSharesDtoFromMap(accountingBridgeData, cashBasedAccountingEnabled,
accrualBasedAccountingEnabled);
final AccountingProcessorForShares accountingProcessorForShares = this.accountingProcessorForSharesFactory
.determineProcessor(sharesDTO);
accountingProcessorForShares.createJournalEntriesForShares(sharesDTO);
}
}
@Override
public void revertShareAccountJournalEntries(final ArrayList<Long> transactionIds, final LocalDate transactionDate) {
for (Long shareTransactionId : transactionIds) {
String transactionId = AccountingProcessorHelper.SHARE_TRANSACTION_IDENTIFIER + shareTransactionId;
List<JournalEntry> journalEntries = this.glJournalEntryRepository.findJournalEntries(transactionId,
PortfolioProductType.SHARES.getValue());
if (journalEntries == null || journalEntries.isEmpty()) {
continue;
}
final Long officeId = journalEntries.get(0).getOffice().getId();
final String reversalTransactionId = generateTransactionId(officeId);
for (final JournalEntry journalEntry : journalEntries) {
JournalEntry reversalJournalEntry;
String reversalComment = "Reversal entry for Journal Entry with id :" + journalEntry.getId() + " and transaction Id "
+ journalEntry.getTransactionId();
if (journalEntry.isDebitEntry()) {
reversalJournalEntry = JournalEntry.createNew(journalEntry.getOffice(), journalEntry.getPaymentDetail(),
journalEntry.getGlAccount(), journalEntry.getCurrencyCode(), reversalTransactionId, Boolean.FALSE,
transactionDate, JournalEntryType.CREDIT, journalEntry.getAmount(), reversalComment,
journalEntry.getEntityType(), journalEntry.getEntityId(), journalEntry.getReferenceNumber(),
journalEntry.getLoanTransactionId(), journalEntry.getSavingsTransactionId(),
journalEntry.getClientTransactionId(), journalEntry.getShareTransactionId());
} else {
reversalJournalEntry = JournalEntry.createNew(journalEntry.getOffice(), journalEntry.getPaymentDetail(),
journalEntry.getGlAccount(), journalEntry.getCurrencyCode(), reversalTransactionId, Boolean.FALSE,
transactionDate, JournalEntryType.DEBIT, journalEntry.getAmount(), reversalComment,
journalEntry.getEntityType(), journalEntry.getEntityId(), journalEntry.getReferenceNumber(),
journalEntry.getLoanTransactionId(), journalEntry.getSavingsTransactionId(),
journalEntry.getClientTransactionId(), journalEntry.getShareTransactionId());
}
// save the reversal entry
helper.persistJournalEntry(reversalJournalEntry);
journalEntry.setReversalJournalEntry(reversalJournalEntry);
journalEntry.setReversed(true);
// save the updated journal entry
helper.persistJournalEntry(journalEntry);
}
}
}
private void validateBusinessRulesForJournalEntries(final JournalEntryCommand command) {
// check if date of Journal entry is valid
final LocalDate transactionDate = command.getTransactionDate();
// shouldn't be in the future
if (DateUtils.isDateInTheFuture(transactionDate)) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.FUTURE_DATE, transactionDate, null, null);
}
// shouldn't be before an accounting closure
final GLClosure latestGLClosure = this.glClosureRepository.getLatestGLClosureByBranch(command.getOfficeId());
if (latestGLClosure != null) {
if (!DateUtils.isBefore(latestGLClosure.getClosingDate(), transactionDate)) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.ACCOUNTING_CLOSED, latestGLClosure.getClosingDate(),
null, null);
}
}
// check if credits and debits are valid
final SingleDebitOrCreditEntryCommand[] credits = command.getCredits();
final SingleDebitOrCreditEntryCommand[] debits = command.getDebits();
// atleast one debit or credit must be present
if (credits == null || credits.length == 0 || debits == null || debits.length == 0) {
throw new JournalEntryInvalidException(GlJournalEntryInvalidReason.NO_DEBITS_OR_CREDITS, null, null, null);
}
checkDebitAndCreditAmounts(credits, debits);
}
private void saveAllDebitOrCreditEntries(final JournalEntryCommand command, final Office office, final PaymentDetail paymentDetail,
final String currencyCode, final LocalDate transactionDate,
final SingleDebitOrCreditEntryCommand[] singleDebitOrCreditEntryCommands, final String transactionId,
final JournalEntryType type, final String referenceNumber) {
final boolean manualEntry = true;
for (final SingleDebitOrCreditEntryCommand singleDebitOrCreditEntryCommand : singleDebitOrCreditEntryCommands) {
final GLAccount glAccount = this.glAccountRepository.findById(singleDebitOrCreditEntryCommand.getGlAccountId())
.orElseThrow(() -> new GLAccountNotFoundException(singleDebitOrCreditEntryCommand.getGlAccountId()));
validateGLAccountForTransaction(glAccount);
String comments = command.getComments();
if (!StringUtils.isBlank(singleDebitOrCreditEntryCommand.getComments())) {
comments = singleDebitOrCreditEntryCommand.getComments();
}
/** Validate current code is appropriate **/
this.organisationCurrencyRepository.findOneWithNotFoundDetection(currencyCode);
final JournalEntry glJournalEntry = JournalEntry.createNew(office, paymentDetail, glAccount, currencyCode, transactionId,
manualEntry, transactionDate, type, singleDebitOrCreditEntryCommand.getAmount(), comments, null, null, referenceNumber,
null, null, null, null);
helper.persistJournalEntry(glJournalEntry);
}
}
/**
* TODO: Need a better implementation with guaranteed uniqueness (but not a long UUID)...maybe something tied to
* system clock..
*/
private String generateTransactionId(final Long officeId) {
final AppUser user = this.context.authenticatedUser();
final Long time = System.currentTimeMillis();
final String uniqueVal = String.valueOf(time) + user.getId() + officeId;
return Long.toHexString(Long.parseLong(uniqueVal));
}
private PlatformDataIntegrityException handleJournalEntryDataIntegrityIssues(final Throwable realCause,
final NonTransientDataAccessException dve) {
log.error("Error occurred.", dve);
throw ErrorHandler.getMappable(dve, "error.msg.glJournalEntry.unknown.data.integrity.issue",
"Unknown data integrity issue with resource Journal Entry: " + realCause.getMessage());
}
@Transactional
@Override
public CommandProcessingResult defineOpeningBalance(final JsonCommand command) {
try {
final JournalEntryCommand journalEntryCommand = this.fromApiJsonDeserializer.commandFromApiJson(command.json());
journalEntryCommand.validateForCreate();
final FinancialActivityAccount financialActivityAccountId = this.financialActivityAccountRepositoryWrapper
.findByFinancialActivityTypeWithNotFoundDetection(300);
final Long contraId = financialActivityAccountId.getGlAccount().getId();
if (contraId == null) {
throw new GeneralPlatformDomainRuleException(
"error.msg.financial.activity.mapping.opening.balance.contra.account.cannot.be.null",
"office-opening-balances-contra-account value can not be null", "office-opening-balances-contra-account");
}
validateJournalEntriesArePostedBefore(contraId);
// check office is valid
final Long officeId = command.longValueOfParameterNamed(JournalEntryJsonInputParams.OFFICE_ID.getValue());
final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
final String currencyCode = command.stringValueOfParameterNamed(JournalEntryJsonInputParams.CURRENCY_CODE.getValue());
validateBusinessRulesForJournalEntries(journalEntryCommand);
/**
* revert old journal entries
*/
final List<String> transactionIdsToBeReversed = this.glJournalEntryRepository.findNonReversedContraTransactionIds(contraId,
officeId);
for (String transactionId : transactionIdsToBeReversed) {
final List<JournalEntry> journalEntries = this.glJournalEntryRepository
.findUnReversedManualJournalEntriesByTransactionId(transactionId);
revertJournalEntry(journalEntries, "defining opening balance");
}
/** Set a transaction Id and save these Journal entries **/
final LocalDate transactionDate = command
.localDateValueOfParameterNamed(JournalEntryJsonInputParams.TRANSACTION_DATE.getValue());
final String transactionId = generateTransactionId(officeId);
saveAllDebitOrCreditOpeningBalanceEntries(journalEntryCommand, office, currencyCode, transactionDate,
journalEntryCommand.getDebits(), transactionId, JournalEntryType.DEBIT, contraId);
saveAllDebitOrCreditOpeningBalanceEntries(journalEntryCommand, office, currencyCode, transactionDate,
journalEntryCommand.getCredits(), transactionId, JournalEntryType.CREDIT, contraId);
return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withOfficeId(officeId)
.withTransactionId(transactionId).build();
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
final Throwable throwable = dve.getMostSpecificCause();
throw handleJournalEntryDataIntegrityIssues(throwable, dve);
}
}
private void saveAllDebitOrCreditOpeningBalanceEntries(final JournalEntryCommand command, final Office office,
final String currencyCode, final LocalDate transactionDate,
final SingleDebitOrCreditEntryCommand[] singleDebitOrCreditEntryCommands, final String transactionId,
final JournalEntryType type, final Long contraAccountId) {
final boolean manualEntry = true;
final GLAccount contraAccount = this.glAccountRepository.findById(contraAccountId)
.orElseThrow(() -> new GLAccountNotFoundException(contraAccountId));
if (!GLAccountType.fromInt(contraAccount.getType()).isEquityType()) {
throw new GeneralPlatformDomainRuleException(
"error.msg.configuration.opening.balance.contra.account.value.is.invalid.account.type",
"Global configuration 'office-opening-balances-contra-account' value is not an equity type account", contraAccountId);
}
validateGLAccountForTransaction(contraAccount);
final JournalEntryType contraType = getContraType(type);
String comments = command.getComments();
/** Validate current code is appropriate **/
this.organisationCurrencyRepository.findOneWithNotFoundDetection(currencyCode);
for (final SingleDebitOrCreditEntryCommand singleDebitOrCreditEntryCommand : singleDebitOrCreditEntryCommands) {
final GLAccount glAccount = this.glAccountRepository.findById(singleDebitOrCreditEntryCommand.getGlAccountId())
.orElseThrow(() -> new GLAccountNotFoundException(singleDebitOrCreditEntryCommand.getGlAccountId()));
validateGLAccountForTransaction(glAccount);
if (!StringUtils.isBlank(singleDebitOrCreditEntryCommand.getComments())) {
comments = singleDebitOrCreditEntryCommand.getComments();
}
final JournalEntry glJournalEntry = JournalEntry.createNew(office, null, glAccount, currencyCode, transactionId, manualEntry,
transactionDate, type, singleDebitOrCreditEntryCommand.getAmount(), comments, null, null, null, null, null, null, null);
helper.persistJournalEntry(glJournalEntry);
final JournalEntry contraEntry = JournalEntry.createNew(office, null, contraAccount, currencyCode, transactionId, manualEntry,
transactionDate, contraType, singleDebitOrCreditEntryCommand.getAmount(), comments, null, null, null, null, null, null,
null);
helper.persistJournalEntry(contraEntry);
}
}
private JournalEntryType getContraType(final JournalEntryType type) {
final JournalEntryType contraType;
if (type.isCreditType()) {
contraType = JournalEntryType.DEBIT;
} else {
contraType = JournalEntryType.CREDIT;
}
return contraType;
}
private void validateJournalEntriesArePostedBefore(final Long contraId) {
final List<String> transactionIds = this.glJournalEntryRepository.findNonContraTransactionIds(contraId);
if (!CollectionUtils.isEmpty(transactionIds)) {
throw new GeneralPlatformDomainRuleException("error.msg.journalentry.defining.openingbalance.not.allowed",
"Defining Opening balances not allowed after journal entries posted", transactionIds);
}
}
@Override
public void createJournalEntriesForClientTransactions(Map<String, Object> accountingBridgeData) {
final ClientTransactionDTO clientTransactionDTO = this.helper.populateClientTransactionDtoFromMap(accountingBridgeData);
accountingProcessorForClientTransactions.createJournalEntriesForClientTransaction(clientTransactionDTO);
}
private static class OfficeCurrencyKey {
final Office office;
final String currency;
OfficeCurrencyKey(Office office, String currency) {
this.office = office;
this.currency = currency;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof OfficeCurrencyKey copy)) {
return false;
}
return Objects.equals(this.office.getId(), copy.office.getId()) && this.currency.equals(copy.currency);
}
@Override
public int hashCode() {
return this.office.hashCode() + this.currency.hashCode();
}
}
}