| /** |
| * 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.investor.service; |
| |
| import java.math.BigDecimal; |
| import java.time.LocalDate; |
| import java.util.ArrayList; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import lombok.RequiredArgsConstructor; |
| import org.apache.fineract.accounting.common.AccountingConstants; |
| import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount; |
| import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper; |
| import org.apache.fineract.accounting.glaccount.domain.GLAccount; |
| import org.apache.fineract.accounting.journalentry.domain.JournalEntry; |
| import org.apache.fineract.accounting.journalentry.domain.JournalEntryType; |
| import org.apache.fineract.investor.accounting.journalentry.service.InvestorAccountingHelper; |
| import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMapping; |
| import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMappingRepository; |
| import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer; |
| import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMapping; |
| import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMappingRepository; |
| import org.apache.fineract.organisation.office.domain.Office; |
| import org.apache.fineract.portfolio.loanaccount.domain.Loan; |
| import org.jetbrains.annotations.NotNull; |
| import org.springframework.stereotype.Service; |
| |
| @Service |
| @RequiredArgsConstructor |
| public class AccountingServiceImpl implements AccountingService { |
| |
| private final InvestorAccountingHelper helper; |
| private final ExternalAssetOwnerTransferJournalEntryMappingRepository externalAssetOwnerTransferJournalEntryMappingRepository; |
| private final ExternalAssetOwnerJournalEntryMappingRepository externalAssetOwnerJournalEntryMappingRepository; |
| private final FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository; |
| |
| private static boolean participateInTransfer(FinancialActivityAccount financialActivityAccount, JournalEntry journalEntry, |
| JournalEntryType filterType) { |
| return filterType.getValue().equals(journalEntry.getType()) |
| && !Objects.equals(financialActivityAccount.getGlAccount().getId(), journalEntry.getGlAccount().getId()); |
| } |
| |
| @Override |
| public void createJournalEntriesForSaleAssetTransfer(final Loan loan, final ExternalAssetOwnerTransfer transfer) { |
| List<JournalEntry> journalEntryList = createJournalEntries(loan, transfer, true); |
| createMappingToTransfer(transfer, journalEntryList); |
| createMappingToOwner(transfer, journalEntryList, JournalEntryType.DEBIT); |
| } |
| |
| @Override |
| public void createJournalEntriesForBuybackAssetTransfer(final Loan loan, final ExternalAssetOwnerTransfer transfer) { |
| List<JournalEntry> journalEntryList = createJournalEntries(loan, transfer, false); |
| createMappingToTransfer(transfer, journalEntryList); |
| createMappingToOwner(transfer, journalEntryList, JournalEntryType.CREDIT); |
| } |
| |
| @NotNull |
| private List<JournalEntry> createJournalEntries(Loan loan, ExternalAssetOwnerTransfer transfer, boolean isReversalOrder) { |
| this.helper.checkForBranchClosures(loan.getOffice().getId(), transfer.getSettlementDate()); |
| // transaction properties |
| final Long transactionId = transfer.getId(); |
| final LocalDate transactionDate = transfer.getSettlementDate(); |
| final BigDecimal principalAmount = loan.getLoanSummary().getTotalPrincipalOutstanding(); |
| final BigDecimal interestAmount = loan.getLoanSummary().getTotalInterestOutstanding(); |
| final BigDecimal feesAmount = loan.getLoanSummary().getTotalFeeChargesOutstanding(); |
| final BigDecimal penaltiesAmount = loan.getLoanSummary().getTotalPenaltyChargesOutstanding(); |
| final BigDecimal overPaymentAmount = loan.getTotalOverpaid(); |
| |
| // Moving money to asset transfer account |
| List<JournalEntry> journalEntryList = createJournalEntries(loan, transactionId, transactionDate, principalAmount, interestAmount, |
| feesAmount, penaltiesAmount, overPaymentAmount, !isReversalOrder); |
| // Moving money from asset transfer account |
| journalEntryList.addAll(createJournalEntries(loan, transactionId, transactionDate, principalAmount, interestAmount, feesAmount, |
| penaltiesAmount, overPaymentAmount, isReversalOrder)); |
| return journalEntryList; |
| } |
| |
| private void createMappingToOwner(ExternalAssetOwnerTransfer transfer, List<JournalEntry> journalEntryList, |
| JournalEntryType filterType) { |
| FinancialActivityAccount financialActivityAccount = this.financialActivityAccountRepository |
| .findByFinancialActivityTypeWithNotFoundDetection(AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue()); |
| journalEntryList.forEach(journalEntry -> { |
| if (participateInTransfer(financialActivityAccount, journalEntry, filterType)) { |
| ExternalAssetOwnerJournalEntryMapping mapping = new ExternalAssetOwnerJournalEntryMapping(); |
| mapping.setJournalEntry(journalEntry); |
| mapping.setOwner(transfer.getOwner()); |
| externalAssetOwnerJournalEntryMappingRepository.saveAndFlush(mapping); |
| } |
| }); |
| } |
| |
| private void createMappingToTransfer(ExternalAssetOwnerTransfer transfer, List<JournalEntry> journalEntryList) { |
| journalEntryList.forEach(journalEntry -> { |
| ExternalAssetOwnerTransferJournalEntryMapping mapping = new ExternalAssetOwnerTransferJournalEntryMapping(); |
| mapping.setJournalEntry(journalEntry); |
| mapping.setOwnerTransfer(transfer); |
| externalAssetOwnerTransferJournalEntryMappingRepository.saveAndFlush(mapping); |
| }); |
| } |
| |
| private List<JournalEntry> createJournalEntries(Loan loan, Long transactionId, LocalDate transactionDate, BigDecimal principalAmount, |
| BigDecimal interestAmount, BigDecimal feesAmount, BigDecimal penaltiesAmount, BigDecimal overPaymentAmount, |
| boolean isReversalOrder) { |
| Long loanProductId = loan.productId(); |
| Long loanId = loan.getId(); |
| Office office = loan.getOffice(); |
| String currencyCode = loan.getCurrencyCode(); |
| List<JournalEntry> journalEntryList = new ArrayList<>(); |
| BigDecimal totalDebitAmount = BigDecimal.ZERO; |
| Map<GLAccount, BigDecimal> accountMap = new LinkedHashMap<>(); |
| // principal entry |
| if (principalAmount != null && principalAmount.compareTo(BigDecimal.ZERO) > 0) { |
| AccountingConstants.AccrualAccountsForLoan accrualAccount = AccountingConstants.AccrualAccountsForLoan.LOAN_PORTFOLIO; |
| if (loan.isChargedOff()) { |
| if (loan.isFraud()) { |
| accrualAccount = AccountingConstants.AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE; |
| } else { |
| accrualAccount = AccountingConstants.AccrualAccountsForLoan.CHARGE_OFF_EXPENSE; |
| } |
| } |
| totalDebitAmount = totalDebitAmount.add(principalAmount); |
| GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, accrualAccount.getValue()); |
| accountMap.put(account, principalAmount); |
| } |
| // interest entry |
| if (interestAmount != null && interestAmount.compareTo(BigDecimal.ZERO) > 0) { |
| AccountingConstants.AccrualAccountsForLoan accrualAccount = AccountingConstants.AccrualAccountsForLoan.INTEREST_RECEIVABLE; |
| if (loan.isChargedOff()) { |
| accrualAccount = AccountingConstants.AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_INTEREST; |
| } |
| totalDebitAmount = totalDebitAmount.add(interestAmount); |
| GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, accrualAccount.getValue()); |
| if (accountMap.containsKey(account)) { |
| BigDecimal amount = accountMap.get(account).add(interestAmount); |
| accountMap.put(account, amount); |
| } else { |
| accountMap.put(account, interestAmount); |
| } |
| } |
| // fee entry |
| if (feesAmount != null && feesAmount.compareTo(BigDecimal.ZERO) > 0) { |
| AccountingConstants.AccrualAccountsForLoan accrualAccount = AccountingConstants.AccrualAccountsForLoan.FEES_RECEIVABLE; |
| if (loan.isChargedOff()) { |
| accrualAccount = AccountingConstants.AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_FEES; |
| } |
| totalDebitAmount = totalDebitAmount.add(feesAmount); |
| GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, accrualAccount.getValue()); |
| if (accountMap.containsKey(account)) { |
| BigDecimal amount = accountMap.get(account).add(feesAmount); |
| accountMap.put(account, amount); |
| } else { |
| accountMap.put(account, feesAmount); |
| } |
| } |
| // penalty entry |
| if (penaltiesAmount != null && penaltiesAmount.compareTo(BigDecimal.ZERO) > 0) { |
| AccountingConstants.AccrualAccountsForLoan accrualAccount = AccountingConstants.AccrualAccountsForLoan.PENALTIES_RECEIVABLE; |
| if (loan.isChargedOff()) { |
| accrualAccount = AccountingConstants.AccrualAccountsForLoan.INCOME_FROM_CHARGE_OFF_PENALTY; |
| } |
| totalDebitAmount = totalDebitAmount.add(penaltiesAmount); |
| GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, accrualAccount.getValue()); |
| if (accountMap.containsKey(account)) { |
| BigDecimal amount = accountMap.get(account).add(penaltiesAmount); |
| accountMap.put(account, amount); |
| } else { |
| accountMap.put(account, penaltiesAmount); |
| } |
| } |
| // overpaid entry |
| if (overPaymentAmount != null && overPaymentAmount.compareTo(BigDecimal.ZERO) > 0) { |
| totalDebitAmount = totalDebitAmount.add(overPaymentAmount); |
| GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId, |
| AccountingConstants.AccrualAccountsForLoan.OVERPAYMENT.getValue()); |
| if (accountMap.containsKey(account)) { |
| BigDecimal amount = accountMap.get(account).add(overPaymentAmount); |
| accountMap.put(account, amount); |
| } else { |
| accountMap.put(account, overPaymentAmount); |
| } |
| } |
| // asset transfer entry |
| for (Map.Entry<GLAccount, BigDecimal> entry : accountMap.entrySet()) { |
| journalEntryList.add(this.helper.createCreditJournalEntryOrReversalForInvestor(office, currencyCode, loanId, transactionId, |
| transactionDate, entry.getValue(), isReversalOrder, entry.getKey())); |
| } |
| if (totalDebitAmount.compareTo(BigDecimal.ZERO) > 0) { |
| journalEntryList.add(this.helper.createDebitJournalEntryOrReversalForInvestor(office, currencyCode, |
| AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue(), loanProductId, loanId, transactionId, transactionDate, |
| totalDebitAmount, isReversalOrder)); |
| } |
| return journalEntryList; |
| } |
| } |