From ed4fba3ace0c65857941e52ef6563a36cd6430ea Mon Sep 17 00:00:00 2001 From: "adam.magyari" Date: Wed, 7 May 2025 13:57:04 +0200 Subject: [PATCH] FINERACT-2232: Capitalized income - Daily amortization --- .../test/helper/WorkFlowJobHelper.java | 5 +- ...talizedIncomeAmortizationBusinessStep.java | 112 ++++++++++++++++++ .../LoanCapitalizedIncomeTest.java | 99 ++++++++++++++++ .../common/loans/LoanTransactionHelper.java | 10 ++ 4 files changed, 224 insertions(+), 2 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/cob/loan/CapitalizedIncomeAmortizationBusinessStep.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java diff --git a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java index 0cc8d211f..fcaf44d89 100644 --- a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java +++ b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/helper/WorkFlowJobHelper.java @@ -49,8 +49,9 @@ public class WorkFlowJobHelper { new BusinessStep().stepName("UPDATE_LOAN_ARREARS_AGING").order(6L), // new BusinessStep().stepName("ADD_PERIODIC_ACCRUAL_ENTRIES").order(7L), // new BusinessStep().stepName("ACCRUAL_ACTIVITY_POSTING").order(8L), // - new BusinessStep().stepName("LOAN_INTEREST_RECALCULATION").order(9L), // - new BusinessStep().stepName("EXTERNAL_ASSET_OWNER_TRANSFER").order(10L)// + new BusinessStep().stepName("CAPITALIZED_INCOME_AMORTIZATION").order(9L), // + new BusinessStep().stepName("LOAN_INTEREST_RECALCULATION").order(10L), // + new BusinessStep().stepName("EXTERNAL_ASSET_OWNER_TRANSFER").order(11L)// ); BusinessStepRequest request = new BusinessStepRequest().businessSteps(businessSteps); Response response = businessStepConfigurationApi.updateJobBusinessStepConfig(WORKFLOW_NAME_LOAN_CLOSE_OF_BUSINESS, request) diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CapitalizedIncomeAmortizationBusinessStep.java b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CapitalizedIncomeAmortizationBusinessStep.java new file mode 100644 index 000000000..b8e1cae23 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/loan/CapitalizedIncomeAmortizationBusinessStep.java @@ -0,0 +1,112 @@ +/** + * 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.cob.loan; + +import jakarta.transaction.Transactional; +import java.math.BigDecimal; +import java.math.MathContext; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent; +import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO; +import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeLoanTransactionDTO; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.mapper.LoanAccountingBridgeMapper; +import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository; +import org.apache.fineract.portfolio.loanaccount.util.CapitalizedIncomeAmortizationUtil; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class CapitalizedIncomeAmortizationBusinessStep implements LoanCOBBusinessStep { + + private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository; + private final BusinessEventNotifierService businessEventNotifierService; + private final JournalEntryWritePlatformService journalEntryWritePlatformService; + private final LoanAccountingBridgeMapper loanAccountingBridgeMapper; + private final LoanTransactionRepository loanTransactionRepository; + private final ExternalIdFactory externalIdFactory; + + @Transactional + @Override + public Loan execute(Loan loan) { + MathContext mc = MoneyHelper.getMathContext(); + LocalDate businessDate = DateUtils.getBusinessLocalDate(); + List balances = capitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId()); + + BigDecimal totalAmortizationAmount = BigDecimal.ZERO; + for (LoanCapitalizedIncomeBalance balance : balances) { + long daysUntilMaturity = businessDate.until(loan.getMaturityDate(), ChronoUnit.DAYS); + Money dailyAmortization = CapitalizedIncomeAmortizationUtil.calculateDailyAmortization( + loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), daysUntilMaturity, balance.getUnrecognizedAmount(), + loan.getCurrency().toData()); + totalAmortizationAmount = totalAmortizationAmount.add(dailyAmortization.getAmount(), mc); + balance.setUnrecognizedAmount(balance.getUnrecognizedAmount().subtract(dailyAmortization.getAmount(), mc)); + } + + capitalizedIncomeBalanceRepository.saveAll(balances); + + if (MathUtil.isGreaterThanZero(totalAmortizationAmount)) { + LoanTransaction transaction = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(), businessDate, + totalAmortizationAmount, externalIdFactory.create()); + loan.addLoanTransaction(transaction); + + transaction = loanTransactionRepository.save(transaction); + loanTransactionRepository.flush(); + + final AccountingBridgeLoanTransactionDTO transactionDTO = loanAccountingBridgeMapper.mapToLoanTransactionData(transaction, + loan.getCurrency().getCode()); + final AccountingBridgeDataDTO accountingBridgeData = new AccountingBridgeDataDTO(loan.getId(), loan.getLoanProduct().getId(), + loan.getOfficeId(), loan.getCurrencyCode(), loan.getSummary().getTotalInterestCharged(), + loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct(), + loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(), loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(), false, + false, false, null, loan.isClosedWrittenOff(), List.of(transactionDTO)); + this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); + + businessEventNotifierService + .notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(transaction)); + } + + return loan; + } + + @Override + public String getEnumStyledName() { + return "CAPITALIZED_INCOME_AMORTIZATION"; + } + + @Override + public String getHumanReadableName() { + return "Capitalized income amortization"; + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java new file mode 100644 index 000000000..be63821c0 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java @@ -0,0 +1,99 @@ +/** + * 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.integrationtests; + +import java.math.BigDecimal; +import java.util.concurrent.atomic.AtomicReference; +import org.apache.fineract.client.models.PostClientsResponse; +import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.integrationtests.common.BusinessStepHelper; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest { + + @BeforeAll + public void setup() { + new BusinessStepHelper().updateSteps("LOAN_CLOSE_OF_BUSINESS", "APPLY_CHARGE_TO_OVERDUE_LOANS", "LOAN_DELINQUENCY_CLASSIFICATION", + "CHECK_LOAN_REPAYMENT_DUE", "CHECK_LOAN_REPAYMENT_OVERDUE", "CHECK_DUE_INSTALLMENTS", "UPDATE_LOAN_ARREARS_AGING", + "ADD_PERIODIC_ACCRUAL_ENTRIES", "ACCRUAL_ACTIVITY_POSTING", "CAPITALIZED_INCOME_AMORTIZATION", + "LOAN_INTEREST_RECALCULATION", "EXTERNAL_ASSET_OWNER_TRANSFER"); + } + + @Test + public void testLoanCapitalizedIncomeAmortization() { + final AtomicReference loanIdRef = new AtomicReference<>(); + + final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressive().enableIncomeCapitalization(true) + .capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT) + .capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION) + .deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue()) + .incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue()) + .capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE)); + + runAt("1 January 2024", () -> { + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024", + 500.0, 7.0, 3, null); + loanIdRef.set(loanId); + + disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024"); + loanTransactionHelper.addCapitalizedIncome(loanId, "1 January 2024", 50.0); + }); + runAt("2 January 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + verifyTransactions(loanId, // + transaction(100.0, "Disbursement", "01 January 2024"), // + transaction(50.0, "Capitalized Income", "01 January 2024"), // + transaction(0.55, "Capitalized Income Amortization", "01 January 2024") // + ); + }); + runAt("3 January 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + verifyTransactions(loanId, // + transaction(100.0, "Disbursement", "01 January 2024"), // + transaction(50.0, "Capitalized Income", "01 January 2024"), // + transaction(0.55, "Capitalized Income Amortization", "01 January 2024"), // + transaction(0.02, "Accrual", "02 January 2024"), // + transaction(0.55, "Capitalized Income Amortization", "02 January 2024") // + ); + + verifyJournalEntries(loanId, // + journalEntry(100, loansReceivableAccount, "DEBIT"), // + journalEntry(100, fundSource, "CREDIT"), // + journalEntry(50, loansReceivableAccount, "DEBIT"), // + journalEntry(50, deferredIncomeLiabilityAccount, "CREDIT"), // + journalEntry(0.55, deferredIncomeLiabilityAccount, "DEBIT"), // + journalEntry(0.55, feeIncomeAccount, "CREDIT"), // + journalEntry(0.02, interestReceivableAccount, "DEBIT"), // + journalEntry(0.02, interestIncomeAccount, "CREDIT"), // + journalEntry(0.55, deferredIncomeLiabilityAccount, "DEBIT"), // + journalEntry(0.55, feeIncomeAccount, "CREDIT") // + ); + }); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 5db5dc9a1..6fcc0b934 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -909,6 +909,16 @@ public class LoanTransactionHelper { "repayment")); } + public PostLoansLoanIdTransactionsResponse addCapitalizedIncome(final Long loanId, final PostLoansLoanIdTransactionsRequest request) { + return Calls + .ok(FineractClientHelper.getFineractClient().loanTransactions.executeLoanTransaction(loanId, request, "capitalizedIncome")); + } + + public PostLoansLoanIdTransactionsResponse addCapitalizedIncome(final Long loanId, final String transactionDate, final double amount) { + return addCapitalizedIncome(loanId, new PostLoansLoanIdTransactionsRequest().transactionAmount(amount) + .transactionDate(transactionDate).dateFormat("dd MMMM yyyy").locale("en")); + } + // TODO: Rewrite to use fineract-client instead! // Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long, // org.apache.fineract.client.models.PostLoansLoanIdRequest)