FINERACT-2232: Capitalized income adjustment
This commit is contained in:
+131
@@ -21,10 +21,12 @@ package org.apache.fineract.accounting.journalentry.service;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.accounting.closure.domain.GLClosure;
|
||||
import org.apache.fineract.accounting.common.AccountingConstants.AccrualAccountsForLoan;
|
||||
@@ -34,11 +36,21 @@ import org.apache.fineract.accounting.journalentry.data.ChargePaymentDTO;
|
||||
import org.apache.fineract.accounting.journalentry.data.GLAccountBalanceHolder;
|
||||
import org.apache.fineract.accounting.journalentry.data.LoanDTO;
|
||||
import org.apache.fineract.accounting.journalentry.data.LoanTransactionDTO;
|
||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
|
||||
import org.apache.fineract.accounting.journalentry.domain.JournalEntryRepository;
|
||||
import org.apache.fineract.accounting.producttoaccountmapping.domain.ProductToGLAccountMapping;
|
||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.organisation.office.domain.Office;
|
||||
import org.apache.fineract.portfolio.PortfolioProductType;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@@ -47,6 +59,9 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
|
||||
private final AccountingProcessorHelper helper;
|
||||
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
|
||||
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
||||
private final JournalEntryRepository journalEntryRepository;
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
|
||||
@Override
|
||||
public void createJournalEntriesForLoan(final LoanDTO loanDTO) {
|
||||
@@ -131,6 +146,10 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
if (transactionType.isCapitalizedIncomeAmortization()) {
|
||||
createJournalEntriesForCapitalizedIncomeAmortization(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
// Handle Capitalized Income Adjustment
|
||||
if (transactionType.isCapitalizedIncomeAdjustment()) {
|
||||
createJournalEntriesForCapitalizedIncomeAdjustment(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +190,118 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
}
|
||||
}
|
||||
|
||||
private void createJournalEntriesForCapitalizedIncomeAdjustment(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
|
||||
final Office office) {
|
||||
// loan properties
|
||||
final Long loanProductId = loanDTO.getLoanProductId();
|
||||
final Long loanId = loanDTO.getLoanId();
|
||||
final String currencyCode = loanDTO.getCurrencyCode();
|
||||
// transaction properties
|
||||
final String transactionId = loanTransactionDTO.getTransactionId();
|
||||
final LocalDate transactionDate = loanTransactionDTO.getTransactionDate();
|
||||
final BigDecimal transactionAmount = loanTransactionDTO.getAmount();
|
||||
final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
|
||||
final BigDecimal interestAmount = loanTransactionDTO.getInterest();
|
||||
final BigDecimal feesAmount = loanTransactionDTO.getFees();
|
||||
final BigDecimal penaltiesAmount = loanTransactionDTO.getPenalties();
|
||||
final BigDecimal overPaymentAmount = loanTransactionDTO.getOverPayment();
|
||||
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
|
||||
final GLAccountBalanceHolder glAccountBalanceHolder = new GLAccountBalanceHolder();
|
||||
|
||||
if (MathUtil.isGreaterThanZero(transactionAmount)) {
|
||||
// Resolve Credit
|
||||
// handle principal payment
|
||||
if (MathUtil.isGreaterThanZero(principalAmount)) {
|
||||
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
|
||||
AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(), paymentTypeId);
|
||||
glAccountBalanceHolder.addToCredit(account, principalAmount);
|
||||
}
|
||||
// handle interest payment
|
||||
if (MathUtil.isGreaterThanZero(interestAmount)) {
|
||||
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
|
||||
AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue(), paymentTypeId);
|
||||
glAccountBalanceHolder.addToCredit(account, interestAmount);
|
||||
}
|
||||
// handle fee payment
|
||||
if (MathUtil.isGreaterThanZero(feesAmount)) {
|
||||
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
|
||||
AccrualAccountsForLoan.FEES_RECEIVABLE.getValue(), paymentTypeId);
|
||||
glAccountBalanceHolder.addToCredit(account, feesAmount);
|
||||
}
|
||||
// handle penalty payment
|
||||
if (MathUtil.isGreaterThanZero(penaltiesAmount)) {
|
||||
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
|
||||
AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue(), paymentTypeId);
|
||||
glAccountBalanceHolder.addToCredit(account, penaltiesAmount);
|
||||
}
|
||||
// handle overpayment
|
||||
if (MathUtil.isGreaterThanZero(overPaymentAmount)) {
|
||||
GLAccount account = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
|
||||
AccrualAccountsForLoan.OVERPAYMENT.getValue(), paymentTypeId);
|
||||
glAccountBalanceHolder.addToCredit(account, overPaymentAmount);
|
||||
}
|
||||
|
||||
// Resolve Debit
|
||||
GLAccount accountIncomeFromCapitalization = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
|
||||
AccrualAccountsForLoan.INCOME_FROM_CAPITALIZATION.getValue(), paymentTypeId);
|
||||
GLAccount accountDeferredIncome = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
|
||||
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), paymentTypeId);
|
||||
|
||||
Optional<LoanTransaction> transactionOpt = loanTransactionRepository.findById(Long.parseLong(transactionId));
|
||||
if (transactionOpt.isEmpty()) {
|
||||
throw new PlatformDataIntegrityException("transaction.for.accounting.not.found", "Transaction for accounting not found");
|
||||
}
|
||||
Optional<LoanTransactionRelation> relationToOriginalTransactionOpt = transactionOpt.get().getLoanTransactionRelations().stream()//
|
||||
.filter(rel -> LoanTransactionRelationTypeEnum.REPLAYED.equals(rel.getRelationType()))//
|
||||
.min(Comparator.comparing(AbstractPersistableCustom::getId));
|
||||
if (relationToOriginalTransactionOpt.isPresent()) {
|
||||
// Reverse-replay case
|
||||
final LoanTransaction originalTransaction = relationToOriginalTransactionOpt.get().getToTransaction();
|
||||
final List<JournalEntry> originalJournalEntries = this.journalEntryRepository.findJournalEntries(
|
||||
AccountingProcessorHelper.LOAN_TRANSACTION_IDENTIFIER + originalTransaction.getId(),
|
||||
PortfolioProductType.LOAN.getValue());
|
||||
|
||||
for (JournalEntry originalJournalEntry : originalJournalEntries) {
|
||||
if (originalJournalEntry.isDebitEntry()) {
|
||||
if (accountIncomeFromCapitalization.getId().equals(originalJournalEntry.getGlAccount().getId())) {
|
||||
glAccountBalanceHolder.addToDebit(accountIncomeFromCapitalization, originalJournalEntry.getAmount());
|
||||
} else if (accountDeferredIncome.getId().equals(originalJournalEntry.getGlAccount().getId())) {
|
||||
glAccountBalanceHolder.addToDebit(accountDeferredIncome, originalJournalEntry.getAmount());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LoanCapitalizedIncomeBalance capitalizedIncomeBalance = loanCapitalizedIncomeBalanceRepository
|
||||
.findBalanceForAdjustment(Long.parseLong(loanTransactionDTO.getTransactionId()));
|
||||
if (MathUtil.isGreaterThan(transactionAmount, capitalizedIncomeBalance.getUnrecognizedAmount())) {
|
||||
BigDecimal amortizedAmount = transactionAmount.subtract(capitalizedIncomeBalance.getUnrecognizedAmount());
|
||||
|
||||
glAccountBalanceHolder.addToDebit(accountIncomeFromCapitalization, amortizedAmount);
|
||||
glAccountBalanceHolder.addToDebit(accountDeferredIncome, transactionAmount.subtract(amortizedAmount));
|
||||
} else {
|
||||
glAccountBalanceHolder.addToDebit(accountDeferredIncome, transactionAmount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create credit entries
|
||||
for (Map.Entry<Long, BigDecimal> creditEntry : glAccountBalanceHolder.getCreditBalances().entrySet()) {
|
||||
if (MathUtil.isGreaterThanZero(creditEntry.getValue())) {
|
||||
GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey());
|
||||
this.helper.createCreditJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
|
||||
creditEntry.getValue(), glAccount);
|
||||
}
|
||||
}
|
||||
// create debit entries
|
||||
for (Map.Entry<Long, BigDecimal> debitEntry : glAccountBalanceHolder.getDebitBalances().entrySet()) {
|
||||
if (MathUtil.isGreaterThanZero(debitEntry.getValue())) {
|
||||
GLAccount glAccount = glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey());
|
||||
this.helper.createDebitJournalEntryForLoan(office, currencyCode, loanId, transactionId, transactionDate,
|
||||
debitEntry.getValue(), glAccount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void createJournalEntriesForCapitalizedIncomeAmortization(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
|
||||
final Office office) {
|
||||
final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff();
|
||||
|
||||
+2
@@ -700,6 +700,8 @@ public class LoanTransactionsApiResource {
|
||||
CommandWrapper commandRequest;
|
||||
if (CommandParameterUtil.is(commandParam, LoanApiConstants.CHARGEBACK_TRANSACTION_COMMAND)) {
|
||||
commandRequest = builder.chargebackTransaction(resolvedLoanId, resolvedTransactionId).build();
|
||||
} else if (CommandParameterUtil.is(commandParam, LoanApiConstants.CAPITALIZED_INCOME_ADJUSTMENT_TRANSACTION_COMMAND)) {
|
||||
commandRequest = builder.capitalizedIncomeAdjustment(resolvedLoanId, resolvedTransactionId).build();
|
||||
} else { // Default to adjust the Loan Transaction
|
||||
commandRequest = builder.adjustTransaction(resolvedLoanId, resolvedTransactionId).build();
|
||||
}
|
||||
|
||||
+2
@@ -730,6 +730,8 @@ final class LoansApiResourceSwagger {
|
||||
public boolean capitalizedIncome;
|
||||
@Schema(example = "false")
|
||||
public boolean capitalizedIncomeAmortization;
|
||||
@Schema(example = "false")
|
||||
public boolean capitalizedIncomeAdjustment;
|
||||
}
|
||||
|
||||
static final class GetLoansLoanIdPaymentDetailData {
|
||||
|
||||
@@ -197,4 +197,5 @@
|
||||
<include file="parts/0176_add_capitalized_income_amortization_transaction.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0177_acc_journal_entry_index.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0178_add_principal_from_capitalized_income_to_loan_summary.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0179_add_capitalized_income_adjustment_transaction.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||
<changeSet id="1" author="fineract">
|
||||
<insert tableName="m_external_event_configuration">
|
||||
<column name="type" value="LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent"/>
|
||||
<column name="enabled" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="r_enum_value">
|
||||
<column name="enum_name" value="transaction_type_enum"/>
|
||||
<column name="enum_id" valueNumeric="37"/>
|
||||
<column name="enum_message_property" value="Capitalized Income Adjustment"/>
|
||||
<column name="enum_value" value="Capitalized Income Adjustment"/>
|
||||
<column name="enum_type" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+4
-2
@@ -106,7 +106,8 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent",
|
||||
"LoanTransactionAccrualActivityPreBusinessEvent", "LoanTransactionInterestRefundPostBusinessEvent",
|
||||
"LoanTransactionInterestRefundPreBusinessEvent", "LoanAccrualAdjustmentTransactionBusinessEvent",
|
||||
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent");
|
||||
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
|
||||
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
@@ -193,7 +194,8 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent",
|
||||
"LoanTransactionAccrualActivityPreBusinessEvent", "LoanTransactionInterestRefundPostBusinessEvent",
|
||||
"LoanTransactionInterestRefundPreBusinessEvent", "LoanAccrualAdjustmentTransactionBusinessEvent",
|
||||
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent");
|
||||
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent",
|
||||
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
|
||||
Reference in New Issue
Block a user