diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java index 43894c0e3..f9a7d6133 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeWritePlatformServiceImpl.java @@ -41,6 +41,7 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance; +import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; @@ -65,6 +66,7 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository; private final ReprocessLoanTransactionsService reprocessLoanTransactionsService; private final LoanBalanceService loanBalanceService; + private final LoanLifecycleStateMachine loanLifecycleStateMachine; private final BusinessEventNotifierService businessEventNotifierService; @Transactional @@ -103,9 +105,12 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc if (noteText != null && !noteText.isEmpty()) { noteWritePlatformService.createLoanTransactionNote(capitalizedIncomeTransaction.getId(), noteText); } + // Post journal entries journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + loanLifecycleStateMachine.determineAndTransition(loan, transactionDate); + businessEventNotifierService .notifyPostBusinessEvent(new LoanCapitalizedIncomeTransactionCreatedBusinessEvent(capitalizedIncomeTransaction)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); @@ -161,6 +166,8 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc MathUtil.negativeToZero(capitalizedIncomeBalance.getUnrecognizedAmount().subtract(transactionAmount))); capitalizedIncomeBalanceRepository.save(capitalizedIncomeBalance); + loanLifecycleStateMachine.determineAndTransition(loan, transactionDate); + businessEventNotifierService.notifyPostBusinessEvent( new LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent(savedCapitalizedIncomeAdjustment)); businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); @@ -203,4 +210,5 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc capitalizedIncomeBalance.setUnrecognizedAmount(capitalizedIncomeTransaction.getAmount()); capitalizedIncomeBalanceRepository.saveAndFlush(capitalizedIncomeBalance); } + } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java index 2cd92df3d..8eb7fbad2 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/ProgressiveLoanAccountConfiguration.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.starter; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository; @@ -52,10 +53,11 @@ public class ProgressiveLoanAccountConfiguration { NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory, LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository, ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanBalanceService loanBalanceService, - BusinessEventNotifierService businessEventNotifierService) { + LoanLifecycleStateMachine loanLifecycleStateMachine, BusinessEventNotifierService businessEventNotifierService) { return new CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository, paymentDetailWritePlatformService, journalEntryPoster, noteWritePlatformService, externalIdFactory, - capitalizedIncomeBalanceRepository, reprocessLoanTransactionsService, loanBalanceService, businessEventNotifierService); + capitalizedIncomeBalanceRepository, reprocessLoanTransactionsService, loanBalanceService, loanLifecycleStateMachine, + businessEventNotifierService); } @Bean 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 index b2d1ae2c4..058bd54dd 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.LocalDate; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import org.apache.fineract.client.models.GetLoansLoanIdResponse; @@ -693,11 +694,19 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest { addRepaymentForLoan(loanId, 50.58, "15 March 2024"); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateLoanSummaryBalances(loanDetails, 0.0, 151.59, 0.0, 150.0, 0.15); + loanTransactionHelper.makeCreditBalanceRefund(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN) .transactionDate("15 March 2024").locale("en").transactionAmount(0.15)); // Validate Loan is Closed - GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateLoanSummaryBalances(loanDetails, 0.0, 151.59, 0.0, 150.0, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 49.71, 49.71, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 49.99, 49.99, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 50.30, 50.30, 0.0, 50.43, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); }); runAt("16 March 2024", () -> { @@ -709,6 +718,223 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest { verifyTRJournalEntries(capitalizedIncomeTransactionId, journalEntry(50, loansReceivableAccount, "DEBIT"), // journalEntry(50, deferredIncomeLiabilityAccount, "CREDIT") // ); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateLoanSummaryBalances(loanDetails, 50.15, 151.59, 50.00, 150.00, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 49.71, 49.71, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 49.99, 49.99, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 100.30, 50.3, 50.00, 50.43, 0.0); + // Validate Loan is Active + assertTrue(loanDetails.getStatus().getActive()); + }); + } + + @Test + public void testLoanCapitalizedIncomeOnLoanOverpaid() { + final AtomicReference loanIdRef = new AtomicReference<>(); + final AtomicReference capitalizedIncomeTransactionIdRef = 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"); + Long capitalizedIncomeTransactionId = loanTransactionHelper.addCapitalizedIncome(loanId, "1 January 2024", 50.0) + .getResourceId(); + capitalizedIncomeTransactionIdRef.set(capitalizedIncomeTransactionId); + }); + runAt("1 February 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + addRepaymentForLoan(loanId, 50.58, "1 February 2024"); + }); + runAt("1 March 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + addRepaymentForLoan(loanId, 50.58, "1 March 2024"); + }); + runAt("15 March 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + addRepaymentForLoan(loanId, 50.58, "15 March 2024"); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateLoanSummaryBalances(loanDetails, 0.0, 151.59, 0.0, 150.0, 0.15); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 49.71, 49.71, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 49.99, 49.99, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 50.30, 50.30, 0.0, 50.43, 0.0); + // Validate Loan is Overpaid + assertTrue(loanDetails.getStatus().getOverpaid()); + }); + runAt("16 March 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + Long capitalizedIncomeTransactionId = loanTransactionHelper.addCapitalizedIncome(loanId, "16 March 2024", 50.0).getResourceId(); + + verifyTRJournalEntries(capitalizedIncomeTransactionId, journalEntry(50, loansReceivableAccount, "DEBIT"), // + journalEntry(50, deferredIncomeLiabilityAccount, "CREDIT") // + ); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateLoanSummaryBalances(loanDetails, 50.0, 151.74, 49.85, 150.15, null); + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 49.71, 49.71, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 49.99, 49.99, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 100.30, 50.45, 49.85, 50.58, 0.0); + + assertTrue(loanDetails.getStatus().getActive()); + }); + } + + @Test + public void testLoanCapitalizedIncomeAdjustmentOnLoanOverpaid() { + final AtomicReference loanIdRef = new AtomicReference<>(); + final AtomicReference capitalizedIncomeTransactionIdRef = 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"); + Long capitalizedIncomeTransactionId = loanTransactionHelper.addCapitalizedIncome(loanId, "1 January 2024", 50.0) + .getResourceId(); + capitalizedIncomeTransactionIdRef.set(capitalizedIncomeTransactionId); + }); + runAt("1 February 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + addRepaymentForLoan(loanId, 50.58, "1 February 2024"); + }); + runAt("1 March 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + addRepaymentForLoan(loanId, 50.58, "1 March 2024"); + }); + runAt("1 April 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + addRepaymentForLoan(loanId, 60.6, "1 April 2024"); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + // Validate Loan is Overpaid + assertTrue(loanDetails.getStatus().getOverpaid()); + }); + runAt("5 April 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + loanTransactionHelper.makeCreditBalanceRefund(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN) + .transactionDate("5 April 2024").locale("en").transactionAmount(10.00)); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + // Validate Loan remains Overpaid + assertTrue(loanDetails.getStatus().getOverpaid()); + }); + runAt("15 April 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + Long capitalizedIncomeAdjustmentTransactionId = loanTransactionHelper + .capitalizedIncomeAdjustment(loanId, capitalizedIncomeTransactionIdRef.get(), "15 April 2024", 15.0).getResourceId(); + verifyTRJournalEntries(capitalizedIncomeAdjustmentTransactionId, journalEntry(15, feeIncomeAccount, "DEBIT"), // + journalEntry(15, overpaymentAccount, "CREDIT") // + ); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + // Validate Loan remains Overpaid + assertTrue(loanDetails.getStatus().getOverpaid()); + validateLoanSummaryBalances(loanDetails, 0.0, 151.75, 0.0, 150.00, 15.01); + }); + } + + @Test + public void testLoanCapitalizedIncomeAdjustmentOnLoanClosed() { + final AtomicReference loanIdRef = new AtomicReference<>(); + final AtomicReference capitalizedIncomeTransactionIdRef = 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"); + Long capitalizedIncomeTransactionId = loanTransactionHelper.addCapitalizedIncome(loanId, "1 January 2024", 50.0) + .getResourceId(); + capitalizedIncomeTransactionIdRef.set(capitalizedIncomeTransactionId); + }); + runAt("1 February 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + addRepaymentForLoan(loanId, 50.58, "1 February 2024"); + }); + runAt("1 March 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + addRepaymentForLoan(loanId, 50.58, "1 March 2024"); + }); + runAt("1 April 2024", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + addRepaymentForLoan(loanId, 50.59, "1 April 2024"); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateLoanSummaryBalances(loanDetails, 0.0, 151.75, 0.0, 150.00, null); + // Validate Loan goes to Closed + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + }); + runAt("15 April 2024", () -> { + Long loanId = loanIdRef.get(); + Long capitalizedIncomeAdjustmentTransactionId = loanTransactionHelper + .capitalizedIncomeAdjustment(loanId, capitalizedIncomeTransactionIdRef.get(), "15 April 2024", 15.0).getResourceId(); + verifyTRJournalEntries(capitalizedIncomeAdjustmentTransactionId, journalEntry(15, feeIncomeAccount, "DEBIT"), // + journalEntry(15.00, overpaymentAccount, "CREDIT") // + ); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateLoanSummaryBalances(loanDetails, 0.0, 151.75, 0.0, 150.00, 15.0); + // Validate Loan goes to Overpaid + assertTrue(loanDetails.getStatus().getOverpaid()); }); }