From aac62a92bb21b916cf30dbb5d0d87c6282785b48 Mon Sep 17 00:00:00 2001 From: "mark.vituska" Date: Thu, 8 May 2025 10:14:15 +0200 Subject: [PATCH] FINERACT-2232: add income capitalization principal handling and repayment schedule calculations on loans --- .../service/AcmeNoteWritePlatformService.java | 4 +- .../main/avro/loan/v1/LoanSummaryDataV1.avsc | 24 +++ .../core/service/DateUtils.java | 6 + .../service/NoteWritePlatformService.java | 2 +- .../features/LoanCapitalizedIncome.feature | 78 +++++----- .../loanaccount/data/DisbursementData.java | 5 +- .../loanaccount/data/LoanSummaryData.java | 3 + .../loanaccount/domain/LoanSummary.java | 25 +++- .../LoanTransactionRepaymentPeriodData.java | 53 +++++++ .../serialization/LoanRefundValidator.java | 5 +- .../CapitalizedIncomeBalanceService.java | 29 ++++ .../CapitalizedIncomePlatformService.java | 0 .../service/LoanBalanceService.java | 9 +- .../LoanDownPaymentHandlerServiceImpl.java | 5 +- .../service/LoanReadPlatformService.java | 4 +- ...edPaymentScheduleTransactionProcessor.java | 89 ++++++++--- ...oanCapitalizedIncomeBalanceRepository.java | 14 ++ .../CapitalizedIncomeBalanceServiceImpl.java | 45 ++++++ ...talizedIncomeWritePlatformServiceImpl.java | 22 ++- .../ProgressiveLoanAccountConfiguration.java | 16 +- .../loanproduct/calc/EMICalculator.java | 6 + .../calc/ProgressiveEMICalculator.java | 39 ++++- .../calc/data/EmiChangeOperation.java | 10 +- .../loanproduct/calc/data/InterestPeriod.java | 22 ++- .../ProgressiveLoanInterestScheduleModel.java | 13 +- .../calc/data/RepaymentPeriod.java | 17 ++- .../importhandler/loan/LoanImportHandler.java | 1 + .../loanaccount/api/LoansApiResource.java | 11 +- .../api/LoansApiResourceSwagger.java | 4 + .../CommonLoanSummaryDataProvider.java | 4 +- ...comeAmortizationProcessingServiceImpl.java | 28 ++-- .../service/LoanReadPlatformServiceImpl.java | 140 ++++++++++++++---- .../adjustment/LoanAdjustmentServiceImpl.java | 11 +- ...WritePlatformServiceJpaRepositoryImpl.java | 10 +- .../db/changelog/tenant/changelog-tenant.xml | 1 + ...rom_capitalized_income_to_loan_summary.xml | 38 +++++ .../LoanCapitalizedIncomeTest.java | 6 +- .../integrationtests/LoanSummaryTest.java | 35 +++++ 38 files changed, 686 insertions(+), 148 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepaymentPeriodData.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeBalanceService.java rename {fineract-progressive-loan => fineract-loan}/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java (100%) create mode 100644 fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeBalanceServiceImpl.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0178_add_principal_from_capitalized_income_to_loan_summary.xml diff --git a/custom/acme/note/service/src/main/java/com/acme/fineract/portfolio/note/service/AcmeNoteWritePlatformService.java b/custom/acme/note/service/src/main/java/com/acme/fineract/portfolio/note/service/AcmeNoteWritePlatformService.java index 80849842f..14b194f20 100644 --- a/custom/acme/note/service/src/main/java/com/acme/fineract/portfolio/note/service/AcmeNoteWritePlatformService.java +++ b/custom/acme/note/service/src/main/java/com/acme/fineract/portfolio/note/service/AcmeNoteWritePlatformService.java @@ -43,8 +43,8 @@ public class AcmeNoteWritePlatformService implements NoteWritePlatformService, I } @Override - public void createLoanNote(Long loanId, String note) { - throw new UnsupportedOperationException("createLoanNote() is not yet implemented."); + public void createLoanTransactionNote(Long loanTransactionId, String note) { + throw new UnsupportedOperationException("createLoanTransactionNote() is not yet implemented."); } @Override diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanSummaryDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanSummaryDataV1.avsc index 9ad816a55..724d61cf0 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanSummaryDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanSummaryDataV1.avsc @@ -458,6 +458,30 @@ "null", "bigdecimal" ] + }, + { + "default": null, + "name": "totalPrincipal", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalCapitalizedIncome", + "type": [ + "null", + "bigdecimal" + ] + }, + { + "default": null, + "name": "totalCapitalizedIncomeAdjustment", + "type": [ + "null", + "bigdecimal" + ] } ] } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java index ac6320aff..bdf4957a8 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java @@ -310,6 +310,12 @@ public final class DateUtils { return isBeforeBusinessDate(localDate); } + public static boolean isDateInRangeFromInclusiveToExclusive(final LocalDate fromInclusive, final LocalDate upToNotInclusive, + final LocalDate target) { + return (DateUtils.isEqual(target, fromInclusive) || DateUtils.isAfter(target, fromInclusive)) + && DateUtils.isBefore(target, upToNotInclusive); + } + public static int compare(LocalDate first, LocalDate second) { return compare(first, second, true); } diff --git a/fineract-core/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformService.java b/fineract-core/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformService.java index 7ec9177ce..1d8afbd1f 100644 --- a/fineract-core/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformService.java +++ b/fineract-core/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformService.java @@ -26,7 +26,7 @@ public interface NoteWritePlatformService { CommandProcessingResult createNote(JsonCommand command); - void createLoanNote(Long loanId, String note); + void createLoanTransactionNote(Long loanTransactionId, String note); CommandProcessingResult updateNote(JsonCommand command); diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature index 4fb32bd2a..42a33c761 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature @@ -188,12 +188,12 @@ Feature: Capitalized Income | Type | Account code | Account name | Debit | Credit | | ASSET | 112601 | Loans Receivable | 100.0 | | | LIABILITY | 145024 | Deferred Capitalized Income | | 100.0 | - When Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "2 January 2024" with 900.17 EUR transaction amount and system-generated Idempotency key + When Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "2 January 2024" with 1000.17 EUR transaction amount and system-generated Idempotency key Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | | 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false | | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | - | 02 January 2024 | Repayment | 900.17 | 900.0 | 0.17 | 0.0 | 0.0 | 100.0 | false | + | 02 January 2024 | Repayment | 1000.17| 1000.0 | 0.17 | 0.0 | 0.0 | 0.0 | false | | 02 January 2024 | Accrual | 0.17 | 0.0 | 0.17 | 0.0 | 0.0 | 0.0 | false | | 02 January 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false | Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "02 January 2024" which has the following Journal entries: @@ -222,12 +222,12 @@ Feature: Capitalized Income | Type | Account code | Account name | Debit | Credit | | ASSET | 112601 | Loans Receivable | 100.0 | | | LIABILITY | 145024 | Deferred Capitalized Income | | 100.0 | - When Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "2 January 2024" with 1000 EUR transaction amount and system-generated Idempotency key + When Customer makes "REPAYMENT" transaction with "AUTOPAY" payment type on "2 January 2024" with 1100 EUR transaction amount and system-generated Idempotency key Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | | 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false | | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | - | 02 January 2024 | Repayment | 1000.0 | 900.0 | 0.17 | 0.0 | 0.0 | 100.0 | false | + | 02 January 2024 | Repayment | 1100.0 | 1000.0 | 0.17 | 0.0 | 0.0 | 0.0 | false | | 02 January 2024 | Accrual | 0.17 | 0.0 | 0.17 | 0.0 | 0.0 | 0.0 | false | | 02 January 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false | Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "02 January 2024" which has the following Journal entries: @@ -262,7 +262,7 @@ Feature: Capitalized Income | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | | 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false | | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | - | 02 January 2024 | Close (as written-off) | 905.25 | 900.0 | 5.25 | 0.0 | 0.0 | 100.0 | false | + | 02 January 2024 | Close (as written-off) | 1005.81| 1000.0 | 5.81 | 0.0 | 0.0 | 0.0 | false | | 02 January 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false | Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "02 January 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | @@ -555,16 +555,16 @@ Feature: Capitalized Income And Admin does charge-off the loan with reason "DELINQUENT" on "03 February 2024" Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | - | ASSET | 112601 | Loans Receivable | | 900.0 | - | ASSET | 112603 | Interest/Fee Receivable | | 4.0 | - | EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | | - | INCOME | 404001 | Interest Income Charge Off | 4.0 | | + | ASSET | 112601 | Loans Receivable | | 1000.0 | + | ASSET | 112603 | Interest/Fee Receivable | | 4.58 | + | EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| | + | INCOME | 404001 | Interest Income Charge Off | 4.58 | | Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | | 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false | | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | - | 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false | - | 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | false | + | 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false | + | 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | false | | 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false | Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | @@ -597,16 +597,16 @@ Feature: Capitalized Income And Admin does charge-off the loan on "03 February 2024" Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | - | ASSET | 112601 | Loans Receivable | | 900.0 | - | ASSET | 112603 | Interest/Fee Receivable | | 4.0 | - | EXPENSE | 744037 | Credit Loss/Bad Debt-Fraud | 900.0 | | - | INCOME | 404001 | Interest Income Charge Off | 4.0 | | + | ASSET | 112601 | Loans Receivable | | 1000.0 | + | ASSET | 112603 | Interest/Fee Receivable | | 4.58 | + | EXPENSE | 744037 | Credit Loss/Bad Debt-Fraud | 1000.0| | + | INCOME | 404001 | Interest Income Charge Off | 4.58 | | Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | | 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false | | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | - | 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false | - | 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | false | + | 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false | + | 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | false | | 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false | Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | @@ -639,16 +639,16 @@ Feature: Capitalized Income And Admin does charge-off the loan with reason "DELINQUENT" on "03 February 2024" Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | - | ASSET | 112601 | Loans Receivable | | 900.0 | - | ASSET | 112603 | Interest/Fee Receivable | | 4.0 | - | EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | | - | INCOME | 404001 | Interest Income Charge Off | 4.0 | | + | ASSET | 112601 | Loans Receivable | | 1000.0 | + | ASSET | 112603 | Interest/Fee Receivable | | 4.58 | + | EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| | + | INCOME | 404001 | Interest Income Charge Off | 4.58 | | Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | | 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false | | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | - | 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false | - | 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | false | + | 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false | + | 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | false | | 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false | Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | @@ -681,16 +681,16 @@ Feature: Capitalized Income And Admin does charge-off the loan with reason "DELINQUENT" on "03 February 2024" Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | - | ASSET | 112601 | Loans Receivable | | 900.0 | - | ASSET | 112603 | Interest/Fee Receivable | | 4.0 | - | EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | | - | INCOME | 404001 | Interest Income Charge Off | 4.0 | | + | ASSET | 112601 | Loans Receivable | | 1000.0 | + | ASSET | 112603 | Interest/Fee Receivable | | 4.58 | + | EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| | + | INCOME | 404001 | Interest Income Charge Off | 4.58 | | Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | | 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false | | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | - | 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false | - | 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | false | + | 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false | + | 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | false | | 03 February 2024 | Capitalized Income Amortization | 100.0 | 0.0 | 100.0 | 0.0 | 0.0 | 0.0 | false | Then Loan Transactions tab has a "CAPITALIZED_INCOME_AMORTIZATION" transaction with date "03 February 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | @@ -700,20 +700,20 @@ Feature: Capitalized Income Then Admin does a charge-off undo the loan Then Loan Transactions tab has a "CHARGE_OFF" transaction with date "03 February 2024" which has the following Journal entries: | Type | Account code | Account name | Debit | Credit | - | ASSET | 112601 | Loans Receivable | | 900.0 | - | ASSET | 112603 | Interest/Fee Receivable | | 4.0 | - | EXPENSE | 744007 | Credit Loss/Bad Debt | 900.0 | | - | INCOME | 404001 | Interest Income Charge Off | 4.0 | | - | ASSET | 112601 | Loans Receivable | 900.0 | | - | ASSET | 112603 | Interest/Fee Receivable | 4.0 | | - | EXPENSE | 744007 | Credit Loss/Bad Debt | | 900.0 | - | INCOME | 404001 | Interest Income Charge Off | | 4.0 | + | ASSET | 112601 | Loans Receivable | | 1000.0 | + | ASSET | 112603 | Interest/Fee Receivable | | 4.58 | + | EXPENSE | 744007 | Credit Loss/Bad Debt | 1000.0| | + | INCOME | 404001 | Interest Income Charge Off | 4.58 | | + | ASSET | 112601 | Loans Receivable | 1000.0| | + | ASSET | 112603 | Interest/Fee Receivable | 4.58 | | + | EXPENSE | 744007 | Credit Loss/Bad Debt | | 1000.0 | + | INCOME | 404001 | Interest Income Charge Off | | 4.58 | Then Loan Transactions tab has the following data: | Transaction date | Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance | Reverted | | 01 January 2024 | Disbursement | 900.0 | 0.0 | 0.0 | 0.0 | 0.0 | 900.0 | false | | 02 January 2024 | Capitalized Income | 100.0 | 100.0 | 0.0 | 0.0 | 0.0 | 1000.0 | false | - | 03 February 2024 | Accrual | 4.0 | 0.0 | 4.0 | 0.0 | 0.0 | 0.0 | false | - | 03 February 2024 | Charge-off | 904.0 | 900.0 | 4.0 | 0.0 | 0.0 | 0.0 | true | + | 03 February 2024 | Accrual | 4.58 | 0.0 | 4.58 | 0.0 | 0.0 | 0.0 | false | + | 03 February 2024 | Charge-off | 1004.58| 1000.0 | 4.58 | 0.0 | 0.0 | 0.0 | true | Then Reversed loan capitalized income amortization transaction has the following Journal entries: | Type | Account code | Account name | Debit | Credit | | EXPENSE | 744007 | Credit Loss/Bad Debt | | 100.0 | diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DisbursementData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DisbursementData.java index fdcd5b02d..32c175deb 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DisbursementData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/DisbursementData.java @@ -30,7 +30,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul */ @RequiredArgsConstructor @Getter -public final class DisbursementData implements Comparable { +public final class DisbursementData implements LoanPrincipalRelatedDataHolder, Comparable { private final Long id; private final LocalDate expectedDisbursementDate; @@ -102,8 +102,7 @@ public final class DisbursementData implements Comparable { private boolean occursOnDayFromAndIncludingAndUpTo(final LocalDate fromInclusive, final LocalDate upToNotInclusive, final LocalDate target) { - return (DateUtils.isEqual(target, fromInclusive) || DateUtils.isAfter(target, fromInclusive)) - && DateUtils.isBefore(target, upToNotInclusive); + return DateUtils.isDateInRangeFromInclusiveToExclusive(fromInclusive, upToNotInclusive, target); } public BigDecimal getWaivedChargeAmount() { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java index b1e147a5d..61ba8701d 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java @@ -34,6 +34,9 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData; public class LoanSummaryData { private final CurrencyData currency; + private final BigDecimal totalPrincipal; + private final BigDecimal totalCapitalizedIncome; + private final BigDecimal totalCapitalizedIncomeAdjustment; private final BigDecimal principalDisbursed; private final BigDecimal principalAdjustments; private final BigDecimal principalPaid; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java index 9dcc97934..05d97ccb2 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummary.java @@ -38,6 +38,15 @@ import org.apache.fineract.organisation.monetary.domain.Money; public class LoanSummary { // derived totals fields + @Column(name = "total_principal_derived", scale = 6, precision = 19) + private BigDecimal totalPrincipal; + + @Column(name = "capitalized_income_derived", scale = 6, precision = 19) + private BigDecimal totalCapitalizedIncome; + + @Column(name = "capitalized_income_adjustment_derived", scale = 6, precision = 19) + private BigDecimal totalCapitalizedIncomeAdjustment; + @Column(name = "principal_disbursed_derived", scale = 6, precision = 19) private BigDecimal totalPrincipalDisbursed; @@ -166,6 +175,9 @@ public class LoanSummary { this.totalPenaltyChargesWaived = BigDecimal.ZERO; this.totalPenaltyChargesWrittenOff = BigDecimal.ZERO; this.totalPrincipalAdjustments = BigDecimal.ZERO; + this.totalPrincipal = BigDecimal.ZERO; + this.totalCapitalizedIncome = BigDecimal.ZERO; + this.totalCapitalizedIncomeAdjustment = BigDecimal.ZERO; this.totalPrincipalDisbursed = BigDecimal.ZERO; this.totalPrincipalOutstanding = BigDecimal.ZERO; this.totalPrincipalRepaid = BigDecimal.ZERO; @@ -176,17 +188,20 @@ public class LoanSummary { } public void updateSummary(final MonetaryCurrency currency, final Money principal, - final List repaymentScheduleInstallments, Set charges) { - + final List repaymentScheduleInstallments, Set charges, Money capitalizedIncome, + Money capitalizedIncomeAdjustment) { this.totalPrincipalDisbursed = principal.getAmount(); + this.totalCapitalizedIncome = capitalizedIncome.getAmount(); + this.totalCapitalizedIncomeAdjustment = capitalizedIncomeAdjustment.getAmount(); + this.totalPrincipal = principal.plus(capitalizedIncome).getAmount(); this.totalPrincipalAdjustments = calculateTotalPrincipalAdjusted(repaymentScheduleInstallments, currency).getAmount(); this.totalFeeAdjustments = calculateTotalFeeAdjusted(repaymentScheduleInstallments, currency).getAmount(); this.totalPenaltyAdjustments = calculateTotalPenaltyAdjusted(repaymentScheduleInstallments, currency).getAmount(); this.totalPrincipalRepaid = calculateTotalPrincipalRepaid(repaymentScheduleInstallments, currency).getAmount(); this.totalPrincipalWrittenOff = calculateTotalPrincipalWrittenOff(repaymentScheduleInstallments, currency).getAmount(); - this.totalPrincipalOutstanding = principal.plus(this.totalPrincipalAdjustments).minus(this.totalPrincipalRepaid) - .minus(this.totalPrincipalWrittenOff).getAmount(); + this.totalPrincipalOutstanding = principal.plus(capitalizedIncome).plus(this.totalPrincipalAdjustments) + .minus(this.totalPrincipalRepaid).minus(this.totalPrincipalWrittenOff).getAmount(); final Money totalInterestCharged = calculateTotalInterestCharged(repaymentScheduleInstallments, currency); this.totalInterestCharged = totalInterestCharged.getAmount(); @@ -225,7 +240,7 @@ public class LoanSummary { this.totalPenaltyChargesOutstanding = totalPenaltyChargesCharged.minus(this.totalPenaltyChargesRepaid) .minus(this.totalPenaltyChargesWaived).minus(this.totalPenaltyChargesWrittenOff).getAmount(); - final Money totalExpectedRepayment = Money.of(currency, this.totalPrincipalDisbursed).plus(this.totalInterestCharged) + final Money totalExpectedRepayment = Money.of(currency, this.totalPrincipal).plus(this.totalInterestCharged) .plus(this.totalFeeChargesCharged).plus(this.totalPenaltyChargesCharged); this.totalExpectedRepayment = totalExpectedRepayment.getAmount(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepaymentPeriodData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepaymentPeriodData.java new file mode 100644 index 000000000..f5c734841 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRepaymentPeriodData.java @@ -0,0 +1,53 @@ +/** + * 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.portfolio.loanaccount.domain; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.apache.fineract.portfolio.loanaccount.data.LoanPrincipalRelatedDataHolder; + +/** + * Immutable data object representing a subset of loan transaction data. + */ +@Getter +@EqualsAndHashCode +public class LoanTransactionRepaymentPeriodData implements LoanPrincipalRelatedDataHolder, Serializable { + + private final Long transactionId; + private final Long loanId; + private final LocalDate date; + private final boolean reversed; + private final BigDecimal amount; + private final BigDecimal unrecognizedAmount; + private final BigDecimal feeChargesPortion; + + public LoanTransactionRepaymentPeriodData(Long transactionId, Long loanId, LocalDate date, boolean reversed, BigDecimal amount, + BigDecimal unrecognizedAmount, BigDecimal feeChargesPortion) { + this.transactionId = transactionId; + this.loanId = loanId; + this.date = date; + this.reversed = reversed; + this.amount = amount; + this.unrecognizedAmount = unrecognizedAmount; + this.feeChargesPortion = feeChargesPortion; + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanRefundValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanRefundValidator.java index 25d9f030c..a8f9bad5b 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanRefundValidator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanRefundValidator.java @@ -104,7 +104,10 @@ public final class LoanRefundValidator { if (loan.getLoanProduct().isMultiDisburseLoan() && adjustedTransaction == null) { final BigDecimal totalDisbursed = loan.getDisbursedAmount(); final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments(); - final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted); + final BigDecimal totalCapitalizedIncome = loan.getLoanTransactions(LoanTransaction::isCapitalizedIncome).stream() + .filter(LoanTransaction::isNotReversed).map(LoanTransaction::getPrincipalPortion) + .reduce(BigDecimal.ZERO, BigDecimal::add); + final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted).add(totalCapitalizedIncome); if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0) { final String errorMessage = "The transaction amount cannot exceed threshold."; throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeBalanceService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeBalanceService.java new file mode 100644 index 000000000..4229e3855 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeBalanceService.java @@ -0,0 +1,29 @@ +/** + * 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.portfolio.loanaccount.service; + +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; + +public interface CapitalizedIncomeBalanceService { + + Money calculateCapitalizedIncome(Loan loan); + + Money calculateCapitalizedIncomeAdjustment(Loan loan); +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java similarity index 100% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java rename to fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomePlatformService.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBalanceService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBalanceService.java index cb69a73eb..f9a32190c 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBalanceService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanBalanceService.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; +import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; @@ -35,8 +36,11 @@ import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransact import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class LoanBalanceService { + private final CapitalizedIncomeBalanceService capitalizedIncomeBalanceService; + public Money calculateTotalOverpayment(final Loan loan) { Money totalPaidInRepayments = loan.getTotalPaidInRepayments(); @@ -97,7 +101,10 @@ public class LoanBalanceService { loan.setTotalRecovered(recoveredAmount.getAmountDefaultedToNullIfZero()); final Money principal = loan.getLoanRepaymentScheduleDetail().getPrincipal(); - loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(), loan.getLoanCharges()); + final Money capitalizedIncome = capitalizedIncomeBalanceService.calculateCapitalizedIncome(loan); + final Money capitalizedIncomeAdjustment = capitalizedIncomeBalanceService.calculateCapitalizedIncomeAdjustment(loan); + loan.getSummary().updateSummary(loan.getCurrency(), principal, loan.getRepaymentScheduleInstallments(), loan.getLoanCharges(), + capitalizedIncome, capitalizedIncomeAdjustment); loan.updateLoanOutstandingBalances(); } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java index c7cd77ae7..0f3e2c293 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java @@ -173,7 +173,10 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler if (loan.getLoanProduct().isMultiDisburseLoan()) { final BigDecimal totalDisbursed = loan.getDisbursedAmount(); final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments(); - final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted); + final BigDecimal totalCapitalizedIncome = loan.getLoanTransactions(LoanTransaction::isCapitalizedIncome).stream() + .filter(LoanTransaction::isNotReversed).map(LoanTransaction::getPrincipalPortion) + .reduce(BigDecimal.ZERO, BigDecimal::add); + final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted).add(totalCapitalizedIncome); if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0 && loan.repaymentScheduleDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) { final String errorMessage = "The transaction amount cannot exceed threshold."; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java index cdff9421b..ed2d7176a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformService.java @@ -37,6 +37,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData; import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; @@ -50,7 +51,8 @@ public interface LoanReadPlatformService { LoanAccountData fetchRepaymentScheduleData(LoanAccountData accountData); LoanScheduleData retrieveRepaymentSchedule(Long loanId, RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData, - Collection disbursementData, boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType); + Collection disbursementData, Collection capitalizedIncomeData, + boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType); Collection retrieveLoanTransactions(Long loanId); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index d4cbfcd79..cfbbe223a 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -344,6 +344,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep case REAMORTIZE -> handleReAmortization(loanTransaction, ctx); case REAGE -> handleReAge(loanTransaction, ctx); case ACCRUAL_ACTIVITY -> calculateAccrualActivity(loanTransaction, ctx); + case CAPITALIZED_INCOME -> handleCapitalizedIncome(loanTransaction, ctx); // TODO: Cover rest of the transaction types default -> log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf()); } @@ -1199,18 +1200,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep emiCalculator.addDisbursement(model, transactionDate, amortizableAmount); disbursementTransaction.resetDerivedComponents(); - if (amortizableAmount.isGreaterThanZero()) { - model.repaymentPeriods().forEach(rm -> { - LoanRepaymentScheduleInstallment installment = installments.stream().filter( - ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate)) - .findFirst().orElse(null); - if (installment != null) { - installment.updatePrincipal(rm.getDuePrincipal().getAmount()); - installment.updateInterestCharged(rm.getDueInterest().getAmount()); - installment.updateObligationsMet(currency, transactionDate); - } - }); - } + recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, transactionDate, currency); allocateOverpayment(disbursementTransaction, transactionCtx); } @@ -1222,7 +1212,6 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep List candidateRepaymentInstallments = installments.stream().filter( i -> i.getDueDate().isAfter(disbursementTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional()) .toList(); - int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size(); LoanProductRelatedDetail loanProductRelatedDetail = disbursementTransaction.getLoan().getLoanRepaymentScheduleDetail(); Integer installmentAmountInMultiplesOf = disbursementTransaction.getLoan().getLoanProduct().getInstallmentAmountInMultiplesOf(); Money downPaymentAmount = Money.zero(currency); @@ -1240,7 +1229,73 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep Money amortizableAmount = disbursementTransaction.getAmount(currency).minus(downPaymentAmount); + recalculateRepaymentInstallmentsWithoutEMICalculation(disbursementTransaction, amortizableAmount, candidateRepaymentInstallments, + currency, installmentAmountInMultiplesOf); + allocateOverpayment(disbursementTransaction, transactionCtx); + } + + private void handleCapitalizedIncome(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) { + if (capitalizedIncomeTransaction.getLoan().isInterestBearing()) { + handleCapitalizedIncomeWithEMICalculator(capitalizedIncomeTransaction, transactionCtx); + } else { + handleCapitalizedIncomeWithoutEMICalculator(capitalizedIncomeTransaction, transactionCtx); + } + } + + private void handleCapitalizedIncomeWithEMICalculator(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) { + ProgressiveLoanInterestScheduleModel model; + if (!(transactionCtx instanceof ProgressiveTransactionCtx) + || (model = ((ProgressiveTransactionCtx) transactionCtx).getModel()) == null) { + throw new IllegalStateException("TransactionCtx has no model"); + } + List installments = transactionCtx.getInstallments(); + LocalDate transactionDate = capitalizedIncomeTransaction.getTransactionDate(); + MonetaryCurrency currency = transactionCtx.getCurrency(); + + Money amortizableAmount = capitalizedIncomeTransaction.getAmount(currency); + emiCalculator.addCapitalizedIncome(model, transactionDate, amortizableAmount); + + recalculateRepaymentPeriodsWithEMICalculation(amortizableAmount, model, installments, transactionDate, currency); + allocateOverpayment(capitalizedIncomeTransaction, transactionCtx); + } + + private void handleCapitalizedIncomeWithoutEMICalculator(LoanTransaction capitalizedIncomeTransaction, TransactionCtx transactionCtx) { + MonetaryCurrency currency = transactionCtx.getCurrency(); + List installments = transactionCtx.getInstallments(); + List candidateRepaymentInstallments = installments.stream().filter( + i -> i.getDueDate().isAfter(capitalizedIncomeTransaction.getTransactionDate()) && !i.isDownPayment() && !i.isAdditional()) + .toList(); + Integer installmentAmountInMultiplesOf = capitalizedIncomeTransaction.getLoan().getLoanProduct() + .getInstallmentAmountInMultiplesOf(); + + Money amortizableAmount = capitalizedIncomeTransaction.getAmount(currency); + + recalculateRepaymentInstallmentsWithoutEMICalculation(capitalizedIncomeTransaction, amortizableAmount, + candidateRepaymentInstallments, currency, installmentAmountInMultiplesOf); + allocateOverpayment(capitalizedIncomeTransaction, transactionCtx); + } + + private void recalculateRepaymentPeriodsWithEMICalculation(Money amortizableAmount, ProgressiveLoanInterestScheduleModel model, + List installments, LocalDate transactionDate, MonetaryCurrency currency) { if (amortizableAmount.isGreaterThanZero()) { + model.repaymentPeriods().forEach(rm -> { + LoanRepaymentScheduleInstallment installment = installments.stream().filter( + ri -> ri.getDueDate().equals(rm.getDueDate()) && !ri.isDownPayment() && !ri.getDueDate().isBefore(transactionDate)) + .findFirst().orElse(null); + if (installment != null) { + installment.updatePrincipal(rm.getDuePrincipal().getAmount()); + installment.updateInterestCharged(rm.getDueInterest().getAmount()); + installment.updateObligationsMet(currency, transactionDate); + } + }); + } + } + + private void recalculateRepaymentInstallmentsWithoutEMICalculation(LoanTransaction loanTransaction, Money amortizableAmount, + List candidateRepaymentInstallments, MonetaryCurrency currency, + Integer installmentAmountInMultiplesOf) { + if (amortizableAmount.isGreaterThanZero()) { + int noCandidateRepaymentInstallments = candidateRepaymentInstallments.size(); Money increasePrincipalBy = amortizableAmount.dividedBy(noCandidateRepaymentInstallments, MoneyHelper.getMathContext()); MoneyHolder moneyHolder = new MoneyHolder(amortizableAmount); @@ -1252,14 +1307,12 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep } i.updatePrincipal(newPrincipal.getAmount()); moneyHolder.setMoneyObject(moneyHolder.getMoneyObject().minus(newPrincipal).plus(previousPrincipal)); - i.updateObligationsMet(currency, disbursementTransaction.getTransactionDate()); + i.updateObligationsMet(currency, loanTransaction.getTransactionDate()); }); // Hence the rounding, we might need to amend the last installment amount - candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1) - .addToPrincipal(disbursementTransaction.getTransactionDate(), moneyHolder.getMoneyObject()); + candidateRepaymentInstallments.get(noCandidateRepaymentInstallments - 1).addToPrincipal(loanTransaction.getTransactionDate(), + moneyHolder.getMoneyObject()); } - - allocateOverpayment(disbursementTransaction, transactionCtx); } private void allocateOverpayment(LoanTransaction loanTransaction, TransactionCtx transactionCtx) { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java index b1ac6238c..ab5f0f1ac 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/repository/LoanCapitalizedIncomeBalanceRepository.java @@ -18,15 +18,29 @@ */ package org.apache.fineract.portfolio.loanaccount.repository; +import java.math.BigDecimal; import java.util.List; import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; public interface LoanCapitalizedIncomeBalanceRepository extends JpaRepository, JpaSpecificationExecutor { + String FIND_BALANCE_REPAYMENT_SCHEDULE_DATA = "SELECT new org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData(lcib.loanTransaction.id, lcib.loan.id, lcib.loanTransaction.dateOf, lcib.loanTransaction.reversed, lcib.amount, lcib.unrecognizedAmount, lcib.loanTransaction.feeChargesPortion) FROM LoanCapitalizedIncomeBalance lcib "; + List findAllByLoanId(Long loanId); LoanCapitalizedIncomeBalance findByLoanIdAndLoanTransactionId(Long loanId, Long transactionId); + + @Query(FIND_BALANCE_REPAYMENT_SCHEDULE_DATA + " WHERE lcib.loan.id = :loanId") + List findRepaymentPeriodDataByLoanId(Long loanId); + + @Query("SELECT SUM(lcib.amount) FROM LoanCapitalizedIncomeBalance lcib WHERE lcib.loan.id = :loanId") + BigDecimal calculateCapitalizedIncome(Long loanId); + + @Query("SELECT SUM(lcib.amountAdjustment) FROM LoanCapitalizedIncomeBalance lcib WHERE lcib.loan.id = :loanId") + BigDecimal calculateCapitalizedIncomeAdjustment(Long loanId); } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeBalanceServiceImpl.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeBalanceServiceImpl.java new file mode 100644 index 000000000..60a7271cf --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CapitalizedIncomeBalanceServiceImpl.java @@ -0,0 +1,45 @@ +/** + * 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.portfolio.loanaccount.service; + +import java.math.BigDecimal; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository; + +@Slf4j +@RequiredArgsConstructor +public class CapitalizedIncomeBalanceServiceImpl implements CapitalizedIncomeBalanceService { + + private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository; + + @Override + public Money calculateCapitalizedIncome(Loan loan) { + BigDecimal balance = capitalizedIncomeBalanceRepository.calculateCapitalizedIncome(loan.getId()); + return Money.of(loan.getCurrency(), balance); + } + + @Override + public Money calculateCapitalizedIncomeAdjustment(Loan loan) { + BigDecimal balance = capitalizedIncomeBalanceRepository.calculateCapitalizedIncomeAdjustment(loan.getId()); + return Money.of(loan.getCurrency(), balance); + } +} 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 5c09f015f..de0e94be8 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 @@ -30,6 +30,7 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -54,6 +55,8 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc private final NoteWritePlatformService noteWritePlatformService; private final ExternalIdFactory externalIdFactory; private final LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository; + private final ReprocessLoanTransactionsService reprocessLoanTransactionsService; + private final LoanBalanceService loanBalanceService; @Transactional @Override @@ -74,21 +77,24 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc final Money capitalizedIncomeAmount = calculateCapitalizedIncomeAmount(loan, transactionAmount); final LoanTransaction capitalizedIncomeTransaction = LoanTransaction.capitalizedIncome(loan, capitalizedIncomeAmount, paymentDetail, transactionDate, txnExternalId); - // TODO: Transaction reverse-replay + loan repayment schedule update (follow-up story) // Update loan with capitalized income loan.addLoanTransaction(capitalizedIncomeTransaction); + // Recalculate loan transactions + recalculateLoanTransactions(loan, transactionDate, capitalizedIncomeTransaction); // Create capitalized income balances createCapitalizedIncomeBalance(capitalizedIncomeTransaction); // Save and flush (PK is set) loanTransactionRepository.saveAndFlush(capitalizedIncomeTransaction); + // Create capitalized income balances + createCapitalizedIncomeBalance(capitalizedIncomeTransaction); // Update loan counters and save loan.updateLoanScheduleDependentDerivedFields(); - // Update outstanding loan balances - loan.updateLoanOutstandingBalances(); + // Update loan summary data + loanBalanceService.updateLoanSummaryDerivedFields(loan); // Create a note if provided final String noteText = command.stringValueOfParameterNamed("note"); if (noteText != null && !noteText.isEmpty()) { - noteWritePlatformService.createLoanNote(loanId, noteText); + noteWritePlatformService.createLoanTransactionNote(capitalizedIncomeTransaction.getId(), noteText); } // Post journal entries journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); @@ -99,6 +105,14 @@ public class CapitalizedIncomeWritePlatformServiceImpl implements CapitalizedInc .build(); } + private void recalculateLoanTransactions(Loan loan, LocalDate transactionDate, LoanTransaction capitalizedIncomeTransaction) { + if (loan.isInterestBearingAndInterestRecalculationEnabled() || DateUtils.isBeforeBusinessDate(transactionDate)) { + reprocessLoanTransactionsService.reprocessTransactions(loan); + } else { + reprocessLoanTransactionsService.processLatestTransaction(capitalizedIncomeTransaction, loan); + } + } + @Override public void resetBalance(final Long loanId) { capitalizedIncomeBalanceRepository 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 cf0ef76b2..31e27793b 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 @@ -24,12 +24,16 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository; import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator; +import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeBalanceService; +import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeBalanceServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomePlatformService; import org.apache.fineract.portfolio.loanaccount.service.CapitalizedIncomeWritePlatformServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster; import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidator; import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanTransactionValidatorImpl; +import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService; import org.apache.fineract.portfolio.note.service.NoteWritePlatformService; import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -45,10 +49,11 @@ public class ProgressiveLoanAccountConfiguration { LoanAssembler loanAssembler, LoanTransactionRepository loanTransactionRepository, PaymentDetailWritePlatformService paymentDetailWritePlatformService, LoanJournalEntryPoster journalEntryPoster, NoteWritePlatformService noteWritePlatformService, ExternalIdFactory externalIdFactory, - LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository) { + LoanCapitalizedIncomeBalanceRepository capitalizedIncomeBalanceRepository, + ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanBalanceService loanBalanceService) { return new CapitalizedIncomeWritePlatformServiceImpl(loanTransactionValidator, loanAssembler, loanTransactionRepository, paymentDetailWritePlatformService, journalEntryPoster, noteWritePlatformService, externalIdFactory, - capitalizedIncomeBalanceRepository); + capitalizedIncomeBalanceRepository, reprocessLoanTransactionsService, loanBalanceService); } @Bean @@ -59,4 +64,11 @@ public class ProgressiveLoanAccountConfiguration { return new ProgressiveLoanTransactionValidatorImpl(fromApiJsonHelper, loanTransactionValidator, loanRepositoryWrapper, loanCapitalizedIncomeBalanceRepository); } + + @Bean + @ConditionalOnMissingBean(CapitalizedIncomeBalanceService.class) + public CapitalizedIncomeBalanceService capitalizedIncomeBalanceService( + LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository) { + return new CapitalizedIncomeBalanceServiceImpl(loanCapitalizedIncomeBalanceRepository); + } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java index 44aa869d2..0991245b5 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java @@ -65,6 +65,12 @@ public interface EMICalculator { */ void addDisbursement(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate disbursementDueDate, Money disbursedAmount); + /** + * Applies the capitalized income transaction on the interest model. This method recalculates the EMI amounts from + * the action date. + */ + void addCapitalizedIncome(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDueDate, Money transactionAmount); + /** * Applies the interest rate change on the interest model. This method recalculates the EMI amounts from the action * date. diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index 0a5d5d991..244352df1 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -119,7 +119,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { private void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) { scheduleModel .changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), operation.getAmount(), - scheduleModel.zero()) + scheduleModel.zero(), scheduleModel.zero()) .ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors( getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel, operation)); @@ -140,6 +140,24 @@ public final class ProgressiveEMICalculator implements EMICalculator { return changedRepaymentPeriod.getDueDate(); } + /** + * Add capitalized income to Interest Period + */ + @Override + public void addCapitalizedIncome(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate transactionDueDate, + final Money transactionAmount) { + addCapitalizedIncome(scheduleModel, EmiChangeOperation.capitalizedIncome(transactionDueDate, transactionAmount)); + } + + private void addCapitalizedIncome(final ProgressiveLoanInterestScheduleModel scheduleModel, final EmiChangeOperation operation) { + scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(operation.getSubmittedOnDate(), scheduleModel.zero(), + scheduleModel.zero(), operation.getAmount()).ifPresent((repaymentPeriod) -> { + calculateEMIValueAndRateFactors( + getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, operation.getSubmittedOnDate()), scheduleModel, + operation); + }); + } + @Override public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate newInterestSubmittedOnDate, final BigDecimal newInterestRate) { @@ -151,7 +169,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { scheduleModel.addInterestRate(interestRateChangeEffectiveDate, operation.getInterestRate()); scheduleModel .changeOutstandingBalanceAndUpdateInterestPeriods(interestRateChangeEffectiveDate, scheduleModel.zero(), - scheduleModel.zero()) + scheduleModel.zero(), scheduleModel.zero()) .ifPresent(repaymentPeriod -> calculateEMIValueAndRateFactors( getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel, operation)); @@ -160,8 +178,8 @@ public final class ProgressiveEMICalculator implements EMICalculator { @Override public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate, Money balanceCorrectionAmount) { - scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.zero(), balanceCorrectionAmount) - .ifPresent(repaymentPeriod -> { + scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.zero(), balanceCorrectionAmount, + scheduleModel.zero()).ifPresent(repaymentPeriod -> { calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel); calculateOutstandingBalance(scheduleModel); calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); @@ -237,7 +255,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { private void addCredit(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate transactionDate, Money creditedPrincipalAmount, Money creditedInterestAmount) { - scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount) + scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(transactionDate, scheduleModel.zero(), creditedPrincipalAmount, scheduleModel.zero()) .ifPresent(repaymentPeriod -> { addCreditedAmountsToInterestPeriod(scheduleModel, transactionDate, creditedPrincipalAmount, creditedInterestAmount); calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel); @@ -440,10 +458,13 @@ public final class ProgressiveEMICalculator implements EMICalculator { .reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 101.48 Money totalDisbursedAmount = scheduleModel.repaymentPeriods().stream() .flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount)) - .reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)) // 100 - .plus(scheduleModel.getTotalCreditedPrincipal(), mc); // + .reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 100 + Money totalCapitalizedIncome = scheduleModel.repaymentPeriods().stream() + .flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getCapitalizedIncomePrincipal)) + .reduce(scheduleModel.zero(), (m1, m2) -> m1.plus(m2, mc)); // 100 - Money diff = totalDisbursedAmount.plus(totalDueInterest, mc).minus(totalEMI, mc); + Money diff = totalDisbursedAmount.plus(totalCapitalizedIncome, mc).plus(scheduleModel.getTotalCreditedPrincipal(), mc) + .plus(totalDueInterest, mc).minus(totalEMI, mc); Optional findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()) .reduce((first, second) -> second); findLastUnpaidRepaymentPeriod.ifPresent(repaymentPeriod -> { @@ -809,7 +830,9 @@ public final class ProgressiveEMICalculator implements EMICalculator { return; } final ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.copyWithoutPaidAmounts(); + addDisbursement(scheduleModelCopy, operation.withZeroAmount()); + addCapitalizedIncome(scheduleModelCopy, operation.withZeroAmount()); final LocalDate firstDueDate = repaymentPeriods.get(0).getDueDate(); scheduleModel.copyPeriodsFrom(firstDueDate, scheduleModelCopy.repaymentPeriods(), (newRepaymentPeriod, actualRepaymentPeriod) -> { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiChangeOperation.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiChangeOperation.java index 675cc6b37..96d8e43b8 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiChangeOperation.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/EmiChangeOperation.java @@ -31,7 +31,9 @@ public class EmiChangeOperation { public enum Action { // DISBURSEMENT, // - INTEREST_RATE_CHANGE // + INTEREST_RATE_CHANGE, // + CAPITALIZED_INCOME, // + ; } private final Action action; @@ -48,8 +50,12 @@ public class EmiChangeOperation { return new EmiChangeOperation(EmiChangeOperation.Action.INTEREST_RATE_CHANGE, newInterestSubmittedOnDate, null, newInterestRate); } + public static EmiChangeOperation capitalizedIncome(final LocalDate transactionDueDate, final Money transactionAmount) { + return new EmiChangeOperation(Action.CAPITALIZED_INCOME, transactionDueDate, transactionAmount, null); + } + public EmiChangeOperation withZeroAmount() { - if (action == Action.DISBURSEMENT) { + if (action == Action.DISBURSEMENT || action == Action.CAPITALIZED_INCOME) { return new EmiChangeOperation(action, submittedOnDate, amount.zero(), null); } return null; diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java index 436a9d133..ffa19bf0b 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/InterestPeriod.java @@ -62,6 +62,8 @@ public class InterestPeriod implements Comparable { private Money balanceCorrectionAmount; private Money outstandingLoanBalance; + private Money capitalizedIncomePrincipal; + @JsonExclude private final MathContext mc; private final boolean isPaused; @@ -70,38 +72,38 @@ public class InterestPeriod implements Comparable { return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(), interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(), interestPeriod.getCreditedInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(), - interestPeriod.getOutstandingLoanBalance(), mc, interestPeriod.isPaused()); + interestPeriod.getOutstandingLoanBalance(), interestPeriod.getCapitalizedIncomePrincipal(), mc, interestPeriod.isPaused()); } public static InterestPeriod empty(@NotNull RepaymentPeriod repaymentPeriod, MathContext mc) { - return new InterestPeriod(repaymentPeriod, null, null, null, null, null, null, null, null, null, mc, false); + return new InterestPeriod(repaymentPeriod, null, null, null, null, null, null, null, null, null,null, mc, false); } public static InterestPeriod copy(@NotNull RepaymentPeriod repaymentPeriod, @NotNull InterestPeriod interestPeriod) { return new InterestPeriod(repaymentPeriod, interestPeriod.getFromDate(), interestPeriod.getDueDate(), interestPeriod.getRateFactor(), interestPeriod.getRateFactorTillPeriodDueDate(), interestPeriod.getCreditedPrincipal(), interestPeriod.getCreditedInterest(), interestPeriod.getDisbursementAmount(), interestPeriod.getBalanceCorrectionAmount(), - interestPeriod.getOutstandingLoanBalance(), interestPeriod.getMc(), interestPeriod.isPaused()); + interestPeriod.getOutstandingLoanBalance(), interestPeriod.getCapitalizedIncomePrincipal(), interestPeriod.getMc(), interestPeriod.isPaused()); } public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate, LocalDate dueDate) { final Money zero = repaymentPeriod.getEmi().zero(); - return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, + return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero, zero.getMc(), false); } public static InterestPeriod withEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate, LocalDate dueDate, boolean isPaused) { final Money zero = repaymentPeriod.getEmi().zero(); - return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, + return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero, zero.getMc(), isPaused); } public static InterestPeriod withPausedAndEmptyAmounts(@NotNull RepaymentPeriod repaymentPeriod, @NotNull LocalDate fromDate, LocalDate dueDate) { final Money zero = repaymentPeriod.getEmi().zero(); - return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, + return new InterestPeriod(repaymentPeriod, fromDate, dueDate, BigDecimal.ZERO, BigDecimal.ZERO, zero, zero, zero, zero, zero, zero, zero.getMc(), true); } @@ -126,6 +128,10 @@ public class InterestPeriod implements Comparable { this.creditedInterest = MathUtil.plus(this.creditedInterest, creditedInterest, mc); } + public void addCapitalizedIncomePrincipalAmount(final Money capitalizedIncomePrincipal) { + this.capitalizedIncomePrincipal = MathUtil.plus(this.capitalizedIncomePrincipal, capitalizedIncomePrincipal, mc); + } + public BigDecimal getCalculatedDueInterest() { if (isPaused) { return creditedInterest.getAmount(); @@ -156,6 +162,7 @@ public class InterestPeriod implements Comparable { InterestPeriod previousInterestPeriod = previousRepaymentPeriod.get().getLastInterestPeriod(); this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance()// .plus(previousInterestPeriod.getDisbursementAmount(), mc)// + .plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), mc)// .plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc)// .minus(previousRepaymentPeriod.get().getDuePrincipal(), mc)// .plus(previousRepaymentPeriod.get().getPaidPrincipal(), mc), mc);// @@ -165,6 +172,7 @@ public class InterestPeriod implements Comparable { InterestPeriod previousInterestPeriod = getRepaymentPeriod().getInterestPeriods().get(index - 1); this.outstandingLoanBalance = MathUtil.negativeToZero(previousInterestPeriod.getOutstandingLoanBalance() // .plus(previousInterestPeriod.getBalanceCorrectionAmount(), mc) // + .plus(previousInterestPeriod.getCapitalizedIncomePrincipal(), mc) // .plus(previousInterestPeriod.getDisbursementAmount(), mc)); // } } @@ -173,7 +181,7 @@ public class InterestPeriod implements Comparable { * Include principal like amounts (all disbursement amount + credited principal) */ public Money getCreditedAmounts() { - return getDisbursementAmount().plus(getCreditedPrincipal(), mc); + return getDisbursementAmount().plus(getCreditedPrincipal(), mc).plus(getCapitalizedIncomePrincipal(), mc); } public boolean isFirstInterestPeriod() { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java index beb3a126d..38cde0ba2 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java @@ -175,9 +175,10 @@ public class ProgressiveLoanInterestScheduleModel { } public Optional changeOutstandingBalanceAndUpdateInterestPeriods(final LocalDate balanceChangeDate, - final Money disbursedAmount, final Money correctionAmount) { + final Money disbursedAmount, final Money correctionAmount, final Money capitalizedIncomePrincipal) { return findRepaymentPeriodForBalanceChange(balanceChangeDate).stream()// - .peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount))// + .peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount, + capitalizedIncomePrincipal))// .findFirst();// } @@ -214,7 +215,7 @@ public class ProgressiveLoanInterestScheduleModel { } private Consumer updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate, final Money disbursedAmount, - final Money correctionAmount) { + final Money correctionAmount, final Money capitalizedIncomePrincipal) { return repaymentPeriod -> { final boolean isChangeOnMaturityDate = isLastRepaymentPeriod(repaymentPeriod) && balanceChangeDate.isEqual(repaymentPeriod.getDueDate()); @@ -222,9 +223,10 @@ public class ProgressiveLoanInterestScheduleModel { isChangeOnMaturityDate); if (interestPeriodOptional.isPresent()) { interestPeriodOptional.get().addDisbursementAmount(disbursedAmount); + interestPeriodOptional.get().addCapitalizedIncomePrincipalAmount(capitalizedIncomePrincipal); interestPeriodOptional.get().addBalanceCorrectionAmount(correctionAmount); } else { - insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount); + insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount, capitalizedIncomePrincipal); } }; } @@ -246,7 +248,7 @@ public class ProgressiveLoanInterestScheduleModel { } void insertInterestPeriod(final RepaymentPeriod repaymentPeriod, final LocalDate balanceChangeDate, final Money disbursedAmount, - final Money correctionAmount) { + final Money correctionAmount, Money capitalizedIncomePrincipal) { final InterestPeriod previousInterestPeriod = findPreviousInterestPeriod(repaymentPeriod, balanceChangeDate); final LocalDate originalDueDate = previousInterestPeriod.getDueDate(); final LocalDate newDueDate = calculateNewDueDate(previousInterestPeriod, balanceChangeDate); @@ -254,6 +256,7 @@ public class ProgressiveLoanInterestScheduleModel { previousInterestPeriod.setDueDate(newDueDate); previousInterestPeriod.addDisbursementAmount(disbursedAmount); + previousInterestPeriod.addCapitalizedIncomePrincipalAmount(capitalizedIncomePrincipal); previousInterestPeriod.addBalanceCorrectionAmount(correctionAmount); final InterestPeriod interestPeriod = InterestPeriod.withEmptyAmounts(repaymentPeriod, newDueDate, originalDueDate, isPaused); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java index 2564dc1b9..6382e361c 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/RepaymentPeriod.java @@ -225,6 +225,17 @@ public final class RepaymentPeriod { .reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); // } + /** + * Sum of capitalized income principals + * + * @return + */ + public Money getCapitalizedIncomePrincipal() { + return MathUtil.negativeToZero(interestPeriods.stream() // + .map(InterestPeriod::getCapitalizedIncomePrincipal) // + .reduce(getZero(mc), (value, previous) -> value.plus(previous, mc)), mc); // + } + /** * Gives back due principal + credited principal or paid principal * @@ -278,6 +289,7 @@ public final class RepaymentPeriod { InterestPeriod lastInterestPeriod = getInterestPeriods().get(getInterestPeriods().size() - 1); Money calculatedOutStandingLoanBalance = lastInterestPeriod.getOutstandingLoanBalance() // .plus(lastInterestPeriod.getBalanceCorrectionAmount(), mc) // + .plus(lastInterestPeriod.getCapitalizedIncomePrincipal(), mc) // .plus(lastInterestPeriod.getDisbursementAmount(), mc) // .minus(getDuePrincipal(), mc)// .plus(getPaidPrincipal(), mc);// @@ -305,7 +317,10 @@ public final class RepaymentPeriod { Money totalDisbursedAmount = getInterestPeriods().stream() // .map(InterestPeriod::getDisbursementAmount) // .reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); // - return initialBalance.add(totalDisbursedAmount, mc); + Money totalCapitalizedIncomeAmount = getInterestPeriods().stream() // + .map(InterestPeriod::getCapitalizedIncomePrincipal) // + .reduce(getZero(mc), (m1, m2) -> m1.plus(m2, mc)); // + return initialBalance.add(totalDisbursedAmount, mc).add(totalCapitalizedIncomeAmount, mc); } private Money getZero(MathContext mc) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/loan/LoanImportHandler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/loan/LoanImportHandler.java index a061ce97d..ec811858a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/loan/LoanImportHandler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/bulkimport/importhandler/loan/LoanImportHandler.java @@ -464,6 +464,7 @@ public class LoanImportHandler implements ImportHandler { JsonObject loanRepaymentJsonob = gsonBuilder.create().toJsonTree(loanRepayments.get(rowIndex)).getAsJsonObject(); loanRepaymentJsonob.remove("manuallyReversed"); loanRepaymentJsonob.remove("numberOfRepayments"); + loanRepaymentJsonob.remove("reversed"); String payload = loanRepaymentJsonob.toString(); final CommandWrapper commandRequest = new CommandWrapperBuilder() // .loanRepaymentTransaction(result.getLoanId()) // diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index 295142664..28fcb876e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -139,6 +139,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeTyp import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryBalancesRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.apache.fineract.portfolio.loanaccount.exception.LoanTemplateTypeRequiredException; import org.apache.fineract.portfolio.loanaccount.exception.NotSupportedLoanTemplateTypeException; @@ -150,6 +151,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanSchedul import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleCalculationPlatformService; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository; import org.apache.fineract.portfolio.loanaccount.service.GLIMAccountInfoReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService; @@ -297,6 +299,7 @@ public class LoansApiResource { private final ClientReadPlatformService clientReadPlatformService; private final LoanTermVariationsRepository loanTermVariationsRepository; private final LoanSummaryProviderDelegate loanSummaryProviderDelegate; + private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository; /* * This template API is used for loan approval, ideally this should be invoked on loan that are pending for @@ -498,11 +501,13 @@ public class LoansApiResource { loanBasicDetails.getPageItems().forEach(i -> { if (i.getSummary() != null) { Collection disbursementData = this.loanReadPlatformService.retrieveLoanDisbursementDetails(i.getId()); + List capitalizedIncomeData = this.loanCapitalizedIncomeBalanceRepository + .findRepaymentPeriodDataByLoanId(i.getId()); final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData( i.getTimeline().getExpectedDisbursementDate(), i.getTimeline().getActualDisbursementDate(), i.getCurrency(), i.getPrincipal(), i.getInArrearsTolerance(), i.getFeeChargesAtDisbursementCharged()); final LoanScheduleData repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(i.getId(), - repaymentScheduleRelatedData, disbursementData, i.isInterestRecalculationEnabled(), + repaymentScheduleRelatedData, disbursementData, capitalizedIncomeData, i.isInterestRecalculationEnabled(), LoanScheduleType.fromEnumOptionData(i.getLoanScheduleType())); LoanSummaryDataProvider loanSummaryDataProvider = loanSummaryProviderDelegate .resolveLoanSummaryDataProvider(i.getTransactionProcessingStrategyCode()); @@ -981,13 +986,15 @@ public class LoansApiResource { if (associationParameters.contains(DataTableApiConstant.repaymentScheduleAssociateParamName)) { mandatoryResponseParameters.add(DataTableApiConstant.repaymentScheduleAssociateParamName); + List capitalizedIncomeData = this.loanCapitalizedIncomeBalanceRepository + .findRepaymentPeriodDataByLoanId(resolvedLoanId); final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedData = new RepaymentScheduleRelatedLoanData( loanBasicDetails.getTimeline().getExpectedDisbursementDate(), loanBasicDetails.getTimeline().getActualDisbursementDate(), loanBasicDetails.getCurrency(), loanBasicDetails.getPrincipal(), loanBasicDetails.getInArrearsTolerance(), loanBasicDetails.getFeeChargesAtDisbursementCharged()); repaymentSchedule = this.loanReadPlatformService.retrieveRepaymentSchedule(resolvedLoanId, repaymentScheduleRelatedData, - disbursementData, loanBasicDetails.isInterestRecalculationEnabled(), + disbursementData, capitalizedIncomeData, loanBasicDetails.isInterestRecalculationEnabled(), LoanScheduleType.fromEnumOptionData(loanBasicDetails.getLoanScheduleType())); if (associationParameters.contains(DataTableApiConstant.futureScheduleAssociateParamName) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index 5f9086191..dfc38b76f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -517,7 +517,11 @@ final class LoansApiResourceSwagger { public GetLoansLoanIdCurrency currency; @Schema(example = "1000000.000000") + public BigDecimal totalPrincipal; + @Schema(example = "1000000.000000") public BigDecimal principalDisbursed; + @Schema(example = "1000000.000000") + public BigDecimal totalCapitalizedIncome; @Schema(example = "0.000000") public BigDecimal principalPaid; @Schema(example = "0.00") diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java index bc8a9ef15..0df243289 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java @@ -97,7 +97,9 @@ public abstract class CommonLoanSummaryDataProvider implements LoanSummaryDataPr } return LoanSummaryData.builder().currency(defaultSummaryData.getCurrency()) - .principalDisbursed(defaultSummaryData.getPrincipalDisbursed()) + .principalDisbursed(defaultSummaryData.getPrincipalDisbursed()).totalPrincipal(defaultSummaryData.getTotalPrincipal()) + .totalCapitalizedIncome(defaultSummaryData.getTotalCapitalizedIncome()) + .totalCapitalizedIncomeAdjustment(defaultSummaryData.getTotalCapitalizedIncomeAdjustment()) .principalAdjustments(defaultSummaryData.getPrincipalAdjustments()).principalPaid(defaultSummaryData.getPrincipalPaid()) .principalWrittenOff(defaultSummaryData.getPrincipalWrittenOff()) .principalOutstanding(defaultSummaryData.getPrincipalOutstanding()) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingServiceImpl.java index 040ff7f52..b5e3bea47 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingServiceImpl.java @@ -21,10 +21,12 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; +import java.util.Optional; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; @@ -51,9 +53,10 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L @Transactional public void processCapitalizedIncomeAmortizationOnLoanClosure(@NotNull final Loan loan) { final LocalDate transactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan); - final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, false); - businessEventNotifierService - .notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction)); + final Optional amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, + false); + amortizationTransaction.ifPresent(loanTransaction -> businessEventNotifierService + .notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(loanTransaction))); } @Override @@ -67,13 +70,16 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L transactionDate = DateUtils.getBusinessLocalDate(); } - final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, true); - journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); - businessEventNotifierService - .notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction)); + final Optional amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, + true); + if (amortizationTransaction.isPresent()) { + journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + businessEventNotifierService.notifyPostBusinessEvent( + new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction.get())); + } } - private LoanTransaction createCapitalizedIncomeAmortizationTransaction(final Loan loan, final LocalDate transactionDate, + private Optional createCapitalizedIncomeAmortizationTransaction(final Loan loan, final LocalDate transactionDate, final boolean isChargeOff) { ExternalId externalId = ExternalId.empty(); BigDecimal totalUnrecognizedAmount = BigDecimal.ZERO; @@ -92,13 +98,17 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L balance.setUnrecognizedAmount(BigDecimal.ZERO); } + if (MathUtil.isLessThanOrEqualZero(totalUnrecognizedAmount)) { + return Optional.empty(); + } + final LoanTransaction amortizationTransaction = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(), transactionDate, totalUnrecognizedAmount, externalId); loan.addLoanTransaction(amortizationTransaction); loanTransactionRepository.saveAndFlush(amortizationTransaction); - return amortizationTransaction; + return Optional.of(amortizationTransaction); } @Override diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index ca2154bda..c6636bd14 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -98,6 +98,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanApprovalData; import org.apache.fineract.portfolio.loanaccount.data.LoanChargePaidByData; import org.apache.fineract.portfolio.loanaccount.data.LoanInterestRecalculationData; import org.apache.fineract.portfolio.loanaccount.data.LoanRepaymentScheduleInstallmentData; +import org.apache.fineract.portfolio.loanaccount.data.LoanSchedulePeriodDataWrapper; import org.apache.fineract.portfolio.loanaccount.data.LoanStatusEnumData; import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; @@ -118,6 +119,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; @@ -249,8 +251,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa accountData.getFeeChargesAtDisbursementCharged()); final Collection disbursementData = retrieveLoanDisbursementDetails(accountData.getId()); + List capitalizedIncomeData = loanCapitalizedIncomeBalanceRepository + .findRepaymentPeriodDataByLoanId(accountData.getId()); final LoanScheduleData repaymentSchedule = retrieveRepaymentSchedule(accountData.getId(), repaymentScheduleRelatedData, - disbursementData, accountData.isInterestRecalculationEnabled(), + disbursementData, capitalizedIncomeData, accountData.isInterestRecalculationEnabled(), LoanScheduleType.fromEnumOptionData(accountData.getLoanScheduleType())); accountData.setRepaymentSchedule(repaymentSchedule); return accountData; @@ -259,13 +263,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa @Override public LoanScheduleData retrieveRepaymentSchedule(final Long loanId, final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData, Collection disbursementData, - boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) { + Collection capitalizedIncomeData, boolean isInterestRecalculationEnabled, + LoanScheduleType loanScheduleType) { try { this.context.authenticatedUser(); final LoanScheduleResultSetExtractor fullResultsetExtractor = new LoanScheduleResultSetExtractor( - repaymentScheduleRelatedLoanData, disbursementData, isInterestRecalculationEnabled, loanScheduleType); + repaymentScheduleRelatedLoanData, disbursementData, capitalizedIncomeData, isInterestRecalculationEnabled, + loanScheduleType); final String sql = "select " + fullResultsetExtractor.schema() + " where ls.loan_id = ? order by ls.loan_id, ls.installment"; return this.jdbcTemplate.query(sql, fullResultsetExtractor, loanId); // NOSONAR @@ -749,7 +755,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa + " l.expected_disbursedon_date as expectedDisbursementDate, l.disbursedon_date as actualDisbursementDate, dbu.username as disbursedByUsername, dbu.firstname as disbursedByFirstname, dbu.lastname as disbursedByLastname," + " l.closedon_date as closedOnDate, cbu.username as closedByUsername, cbu.firstname as closedByFirstname, cbu.lastname as closedByLastname, l.writtenoffon_date as writtenOffOnDate, " + " l.expected_firstrepaymenton_date as expectedFirstRepaymentOnDate, l.interest_calculated_from_date as interestChargedFromDate, l.maturedon_date as actualMaturityDate, l.expected_maturedon_date as expectedMaturityDate, " - + " l.principal_amount_proposed as proposedPrincipal, l.principal_amount as principal, l.approved_principal as approvedPrincipal, l.net_disbursal_amount as netDisbursalAmount, l.arrearstolerance_amount as inArrearsTolerance, l.number_of_repayments as numberOfRepayments, l.repay_every as repaymentEvery," + + " l.principal_amount_proposed as proposedPrincipal, l.principal_amount as principal, l.total_principal_derived as totalPrincipal, l.approved_principal as approvedPrincipal, l.net_disbursal_amount as netDisbursalAmount, l.arrearstolerance_amount as inArrearsTolerance, l.number_of_repayments as numberOfRepayments, l.repay_every as repaymentEvery," + " l.grace_on_principal_periods as graceOnPrincipalPayment, l.recurring_moratorium_principal_periods as recurringMoratoriumOnPrincipalPeriods, l.grace_on_interest_periods as graceOnInterestPayment, l.grace_interest_free_periods as graceOnInterestCharged,l.grace_on_arrears_ageing as graceOnArrearsAgeing," + " l.nominal_interest_rate_per_period as interestRatePerPeriod, l.annual_nominal_interest_rate as annualInterestRate, " + " l.repayment_period_frequency_enum as repaymentFrequencyType, l.interest_period_frequency_enum as interestRateFrequencyType, " @@ -764,7 +770,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa + sqlGenerator.escape("name") + " as currencyName, rc.display_symbol as currencyDisplaySymbol, rc.internationalized_name_code as currencyNameCode, " + " l.loan_officer_id as loanOfficerId, s.display_name as loanOfficerName, " - + " l.principal_disbursed_derived as principalDisbursed, l.principal_repaid_derived as principalPaid," + + " l.principal_disbursed_derived as principalDisbursed, l.capitalized_income_derived as totalCapitalizedIncome, l.capitalized_income_adjustment_derived as totalCapitalizedIncomeAdjustment, l.principal_repaid_derived as principalPaid," + " l.principal_adjustments_derived as principalAdjustments, l.principal_writtenoff_derived as principalWrittenOff," + " l.fee_adjustments_derived as feeAdjustments, l.penalty_adjustments_derived as penaltyAdjustments," + " l.principal_outstanding_derived as principalOutstanding, l.interest_charged_derived as interestCharged," @@ -1023,6 +1029,10 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa // loan summary final BigDecimal principalDisbursed = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalDisbursed"); + final BigDecimal totalPrincipal = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalPrincipal"); + final BigDecimal totalCapitalizedIncome = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "totalCapitalizedIncome"); + final BigDecimal totalCapitalizedIncomeAdjustment = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, + "totalCapitalizedIncomeAdjustment"); final BigDecimal principalAdjustments = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalAdjustments"); final BigDecimal principalPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalPaid"); final BigDecimal principalWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalWrittenOff"); @@ -1066,12 +1076,14 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa inArrears = (overdueSinceDate != null); loanSummary = LoanSummaryData.builder().currency(currencyData).principalDisbursed(principalDisbursed) - .principalAdjustments(principalAdjustments).principalPaid(principalPaid).principalWrittenOff(principalWrittenOff) - .principalOutstanding(principalOutstanding).principalOverdue(principalOverdue).interestCharged(interestCharged) - .interestPaid(interestPaid).interestWaived(interestWaived).interestWrittenOff(interestWrittenOff) - .interestOutstanding(interestOutstanding).interestOverdue(interestOverdue).feeChargesCharged(feeChargesCharged) - .feeAdjustments(feeAdjustments).feeChargesDueAtDisbursementCharged(feeChargesDueAtDisbursementCharged) - .feeChargesPaid(feeChargesPaid).feeChargesWaived(feeChargesWaived).feeChargesWrittenOff(feeChargesWrittenOff) + .totalPrincipal(totalPrincipal).totalCapitalizedIncome(totalCapitalizedIncome) + .totalCapitalizedIncomeAdjustment(totalCapitalizedIncomeAdjustment).principalAdjustments(principalAdjustments) + .principalPaid(principalPaid).principalWrittenOff(principalWrittenOff).principalOutstanding(principalOutstanding) + .principalOverdue(principalOverdue).interestCharged(interestCharged).interestPaid(interestPaid) + .interestWaived(interestWaived).interestWrittenOff(interestWrittenOff).interestOutstanding(interestOutstanding) + .interestOverdue(interestOverdue).feeChargesCharged(feeChargesCharged).feeAdjustments(feeAdjustments) + .feeChargesDueAtDisbursementCharged(feeChargesDueAtDisbursementCharged).feeChargesPaid(feeChargesPaid) + .feeChargesWaived(feeChargesWaived).feeChargesWrittenOff(feeChargesWrittenOff) .feeChargesOutstanding(feeChargesOutstanding).feeChargesOverdue(feeChargesOverdue) .penaltyChargesCharged(penaltyChargesCharged).penaltyAdjustments(penaltyAdjustments) .penaltyChargesPaid(penaltyChargesPaid).penaltyChargesWaived(penaltyChargesWaived) @@ -1271,15 +1283,18 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa private final DisbursementData disbursement; private final BigDecimal totalFeeChargesDueAtDisbursement; private final Collection disbursementData; + private final Collection capitalizedIncomeData; private final LoanScheduleType loanScheduleType; private LocalDate lastDueDate; private BigDecimal outstandingLoanPrincipalBalance; private boolean excludePastUnDisbursed; LoanScheduleResultSetExtractor(final RepaymentScheduleRelatedLoanData repaymentScheduleRelatedLoanData, - Collection disbursementData, boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) { + Collection disbursementData, Collection capitalizedIncomeData, + boolean isInterestRecalculationEnabled, LoanScheduleType loanScheduleType) { this.currency = repaymentScheduleRelatedLoanData.getCurrency(); this.disbursement = repaymentScheduleRelatedLoanData.disbursementData(); + this.capitalizedIncomeData = capitalizedIncomeData; this.totalFeeChargesDueAtDisbursement = repaymentScheduleRelatedLoanData.getTotalFeeChargesAtDisbursement(); this.lastDueDate = this.disbursement.disbursementDate(); this.outstandingLoanPrincipalBalance = this.disbursement.getPrincipal(); @@ -1359,10 +1374,15 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate"); final LocalDate obligationsMetOnDate = JdbcSupport.getLocalDate(rs, "obligationsMetOnDate"); final boolean complete = rs.getBoolean("complete"); - BigDecimal disbursedAmount = BigDecimal.ZERO; - disbursedAmount = processDisbursementData(loanScheduleType, disbursementData, fromDate, dueDate, disbursementPeriodIds, - disbursementChargeAmount, waivedChargeAmount, periods); + List combinedDataList = new ArrayList<>(); + combinedDataList.addAll( + collectEligibleDisbursementData(loanScheduleType, disbursementData, fromDate, dueDate, disbursementPeriodIds)); + combinedDataList.addAll(collectEligibleCapitalizedIncomeData(fromDate, dueDate, disbursementPeriodIds)); + combinedDataList.sort(this::sortPeriodDataHolders); + fillLoanSchedulePeriodData(periods, combinedDataList, disbursementChargeAmount, waivedChargeAmount); + + BigDecimal disbursedAmount = calculateDisbursedAmount(combinedDataList); // Add the Charge back or Credits to the initial amount to avoid negative balance final BigDecimal principalCredits = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "principalCredits"); @@ -1473,26 +1493,82 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa totalPaidLate.getAmount(), totalOutstanding.getAmount(), totalCredits.getAmount()); } - private BigDecimal processDisbursementData(LoanScheduleType loanScheduleType, Collection disbursementData, - LocalDate fromDate, LocalDate dueDate, Set disbursementPeriodIds, BigDecimal disbursementChargeAmount, - BigDecimal waivedChargeAmount, List periods) { - BigDecimal disbursedAmount = BigDecimal.ZERO; + private List collectEligibleDisbursementData(LoanScheduleType loanScheduleType, + Collection disbursementData, LocalDate fromDate, LocalDate dueDate, Set disbursementPeriodIds) { + List disbursementDataList = new ArrayList<>(); + // Collect eligible disbursement data for (final DisbursementData data : disbursementData) { boolean isDueForDisbursement = data.isDueForDisbursement(loanScheduleType, fromDate, dueDate); - if (((fromDate.equals(this.disbursement.disbursementDate()) && data.disbursementDate().equals(fromDate)) + boolean isEligible = ((fromDate.equals(this.disbursement.disbursementDate()) && data.disbursementDate().equals(fromDate)) || (fromDate.equals(dueDate) && data.disbursementDate().equals(fromDate)) || canAddDisbursementData(data, isDueForDisbursement, excludePastUnDisbursed)) - && !disbursementPeriodIds.contains(data.getId())) { - disbursedAmount = disbursedAmount.add(data.getPrincipal()); - LoanSchedulePeriodData periodData = createLoanSchedulePeriodData(data, disbursementChargeAmount, waivedChargeAmount); - periods.add(periodData); - this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(periodData.getPrincipalDisbursed()); + && !disbursementPeriodIds.contains(data.getId()); + + if (isEligible) { + disbursementDataList.add(new LoanSchedulePeriodDataWrapper(data, data.disbursementDate(), true)); disbursementPeriodIds.add(data.getId()); } } + return disbursementDataList; + } + + private List collectEligibleCapitalizedIncomeData(LocalDate fromDate, LocalDate dueDate, + Set disbursementPeriodIds) { + List capitalizedIncomeDataList = new ArrayList<>(); + // Collect eligible capitalized income data + for (LoanTransactionRepaymentPeriodData data : capitalizedIncomeData) { + boolean isEligible = canAddCapitalizedIncomeData(data, fromDate, dueDate) + && !disbursementPeriodIds.contains(data.getTransactionId()); + + if (isEligible) { + capitalizedIncomeDataList.add(new LoanSchedulePeriodDataWrapper(data, data.getDate(), false)); + disbursementPeriodIds.add(data.getTransactionId()); + } + } + return capitalizedIncomeDataList; + } + + private void fillLoanSchedulePeriodData(List periods, List combinedDataList, + BigDecimal disbursementChargeAmount, BigDecimal waivedChargeAmount) { + // Process all collected data in chronological order + for (LoanSchedulePeriodDataWrapper dataItem : combinedDataList) { + LoanSchedulePeriodData periodData; + if (dataItem.isDisbursement()) { + // Process disbursement data + DisbursementData data = (DisbursementData) dataItem.getData(); + periodData = createLoanSchedulePeriodData(data, disbursementChargeAmount, waivedChargeAmount); + } else { + // Process capitalized income data + LoanTransactionRepaymentPeriodData data = (LoanTransactionRepaymentPeriodData) dataItem.getData(); + periodData = createLoanSchedulePeriodData(data); + } + + // Common processing for both data types + periods.add(periodData); + this.outstandingLoanPrincipalBalance = this.outstandingLoanPrincipalBalance.add(periodData.getPrincipalDisbursed()); + } + } + + private BigDecimal calculateDisbursedAmount(List combinedDataList) { + BigDecimal disbursedAmount = BigDecimal.ZERO; + for (LoanSchedulePeriodDataWrapper dataItem : combinedDataList) { + if (dataItem.isDisbursement()) { + DisbursementData data = (DisbursementData) dataItem.getData(); + disbursedAmount = disbursedAmount.add(data.getPrincipal()); + } + } return disbursedAmount; } + private int sortPeriodDataHolders(LoanSchedulePeriodDataWrapper item1, LoanSchedulePeriodDataWrapper item2) { + int dateComparison = item1.getDate().compareTo(item2.getDate()); + if (dateComparison == 0 && item1.isDisbursement() != item2.isDisbursement()) { + // If dates are equal, prioritize disbursement data + return item1.isDisbursement() ? -1 : 1; + } + return dateComparison; + } + private LoanSchedulePeriodData createLoanSchedulePeriodData(final DisbursementData data, BigDecimal disbursementChargeAmount, BigDecimal waivedChargeAmount) { BigDecimal chargeAmount = data.getChargeAmount() == null ? disbursementChargeAmount @@ -1501,11 +1577,20 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa data.isDisbursed()); } + private LoanSchedulePeriodData createLoanSchedulePeriodData(final LoanTransactionRepaymentPeriodData data) { + BigDecimal feeCharges = Objects.isNull(data.getFeeChargesPortion()) ? BigDecimal.ZERO : data.getFeeChargesPortion(); + return LoanSchedulePeriodData.disbursementOnlyPeriod(data.getDate(), data.getAmount(), feeCharges, !data.isReversed()); + } + private boolean canAddDisbursementData(DisbursementData data, boolean isDueForDisbursement, boolean excludePastUnDisbursed) { return (!excludePastUnDisbursed || data.isDisbursed() || !DateUtils.isBeforeBusinessDate(data.disbursementDate())) && isDueForDisbursement; } + private boolean canAddCapitalizedIncomeData(LoanTransactionRepaymentPeriodData data, LocalDate fromDate, LocalDate dueDate) { + return !data.isReversed() && DateUtils.isDateInRangeFromInclusiveToExclusive(fromDate, dueDate, data.getDate()); + } + } private static final class LoanTransactionsMapper implements RowMapper { @@ -2168,7 +2253,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa final LoanTransactionEnumData transactionType = LoanEnumerations.transactionType(LoanTransactionType.REPAYMENT); final Collection paymentTypeOptions = this.paymentTypeReadPlatformService.retrieveAllPaymentTypes(); final BigDecimal outstandingLoanBalance = loanRepaymentScheduleInstallment.getPrincipalOutstanding(currency).getAmount(); - final Boolean isReversed = false; + final Boolean isManuallyReversed = false; final Money outStandingAmount = loanRepaymentScheduleInstallment.getTotalOutstanding(currency); @@ -2178,7 +2263,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa loanRepaymentScheduleInstallment.getInterestOutstanding(currency).getAmount(), loanRepaymentScheduleInstallment.getFeeChargesOutstanding(currency).getAmount(), loanRepaymentScheduleInstallment.getPenaltyChargesOutstanding(currency).getAmount(), null, unrecognizedIncomePortion, - paymentTypeOptions, ExternalId.empty(), null, null, outstandingLoanBalance, isReversed, loanId, loan.getExternalId()); + paymentTypeOptions, ExternalId.empty(), null, null, outstandingLoanBalance, isManuallyReversed, loanId, + loan.getExternalId()); } private static final class CurrencyMapper implements RowMapper { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java index 9b00b712a..dabf4eae2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java @@ -64,6 +64,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanBalanceService; import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService; import org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; +import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService; import org.apache.fineract.portfolio.note.domain.Note; import org.apache.fineract.portfolio.note.domain.NoteRepository; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; @@ -93,6 +94,7 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService { private final LoanChargeValidator loanChargeValidator; private final LoanJournalEntryPoster journalEntryPoster; private final LoanBalanceService loanBalanceService; + private final ReprocessLoanTransactionsService reprocessLoanTransactionsService; @Override public CommandProcessingResult adjustLoanTransaction(Loan loan, LoanTransaction transactionToAdjust, LoanAdjustmentParameter parameter, @@ -245,8 +247,9 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService { transactionForAdjustment.getTransactionDate()); if (!transactionForAdjustment.isAccrualRelated() && transactionForAdjustment.isNotRepaymentLikeType() - && transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund()) { - final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver, accrual or credit balance refund can be adjusted."; + && transactionForAdjustment.isNotWaiver() && transactionForAdjustment.isNotCreditBalanceRefund() + && !transactionForAdjustment.isCapitalizedIncome()) { + final String errorMessage = "Only (non-reversed) transactions of type repayment, waiver, capitalized income, accrual or credit balance refund can be adjusted."; throw new InvalidLoanTransactionTypeException("transaction", "adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions", errorMessage); } @@ -285,6 +288,10 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService { loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, newTransactionDetail, transactionForAdjustment, scheduleGeneratorDTO); } + + if (transactionForAdjustment.getTypeOf().equals(LoanTransactionType.CAPITALIZED_INCOME)) { + reprocessLoanTransactionsService.reprocessTransactions(loan); + } } private Loan saveAndFlushLoanWithDataIntegrityViolationChecks(final Loan loan) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformServiceJpaRepositoryImpl.java index 5d50d8197..44500747e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/note/service/NoteWritePlatformServiceJpaRepositoryImpl.java @@ -204,9 +204,13 @@ public class NoteWritePlatformServiceJpaRepositoryImpl implements NoteWritePlatf } @Override - public void createLoanNote(final Long loanId, final String note) { - final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId); - final Note newNote = Note.loanNote(loan, note); + public void createLoanTransactionNote(final Long loanTransactionId, final String note) { + final LoanTransaction loanTransaction = this.loanTransactionRepository.findById(loanTransactionId) + .orElseThrow(() -> new LoanTransactionNotFoundException(loanTransactionId)); + + final Loan loan = loanTransaction.getLoan(); + + final Note newNote = Note.loanTransactionNote(loan, loanTransaction, note); this.noteRepository.save(newNote); } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 61fb03607..af37175c5 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -196,4 +196,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0178_add_principal_from_capitalized_income_to_loan_summary.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0178_add_principal_from_capitalized_income_to_loan_summary.xml new file mode 100644 index 000000000..d461121b6 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0178_add_principal_from_capitalized_income_to_loan_summary.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + 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 be63821c0..8d96e0b5e 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 @@ -78,7 +78,7 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest { 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.03, "Accrual", "02 January 2024"), // transaction(0.55, "Capitalized Income Amortization", "02 January 2024") // ); @@ -89,8 +89,8 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest { 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.03, interestReceivableAccount, "DEBIT"), // + journalEntry(0.03, interestIncomeAccount, "CREDIT"), // journalEntry(0.55, deferredIncomeLiabilityAccount, "DEBIT"), // journalEntry(0.55, feeIncomeAccount, "CREDIT") // ); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSummaryTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSummaryTest.java index 19f0e11d0..4b18ac9f0 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSummaryTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSummaryTest.java @@ -19,7 +19,9 @@ package org.apache.fineract.integrationtests; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.List; +import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansResponse; @@ -248,4 +250,37 @@ public class LoanSummaryTest extends BaseLoanIntegrationTest { }); } + @Test + public void testCapitalizedIncomeExistsInRepaymentScheduleAndModifiesPrincipal() { + runAt("01 March 2023", () -> { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressiveWithCapitalizedIncome()); + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, + loanProductsResponse.getResourceId(), "01 March 2023", 10000.00, 12.00, 4, null)); + loanId = postLoansResponse.getLoanId(); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(10000.00, "01 March 2023")); + disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 March 2023"); + }); + runAt("02 March 2023", () -> { + loanTransactionHelper.addCapitalizedIncome(loanId, "02 March 2023", 100.00); + }); + + BigDecimal thousand = BigDecimal.valueOf(1000.0); + BigDecimal hundred = BigDecimal.valueOf(100.0); + BigDecimal thousandOneHundred = BigDecimal.valueOf(1100.0); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + + Assertions.assertEquals(thousand, loanDetails.getPrincipal().setScale(1, RoundingMode.HALF_UP)); + Assertions.assertEquals(thousand, loanDetails.getSummary().getPrincipalDisbursed().setScale(1, RoundingMode.HALF_UP)); + Assertions.assertEquals(hundred, loanDetails.getSummary().getTotalCapitalizedIncome().setScale(1, RoundingMode.HALF_UP)); + Assertions.assertEquals(thousandOneHundred, loanDetails.getSummary().getTotalPrincipal().setScale(1, RoundingMode.HALF_UP)); + Assertions.assertEquals(thousandOneHundred, loanDetails.getSummary().getPrincipalOutstanding().setScale(1, RoundingMode.HALF_UP)); + + List periods = loanDetails.getRepaymentSchedule().getPeriods(); + Assertions.assertEquals(6, periods.size()); + Assertions.assertEquals(thousand, periods.get(0).getPrincipalLoanBalanceOutstanding().setScale(1, RoundingMode.HALF_UP)); + Assertions.assertEquals(hundred, periods.get(1).getPrincipalLoanBalanceOutstanding().setScale(1, RoundingMode.HALF_UP)); + } + }