This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new e69378637d FINERACT-2232: Capitalized income - amortization in case of
Preclose event
e69378637d is described below
commit e69378637d2e032cb2674c309dd5f839dc4347d3
Author: mariiaKraievska <[email protected]>
AuthorDate: Fri May 9 13:18:25 2025 +0300
FINERACT-2232: Capitalized income - amortization in case of Preclose event
---
.../apache/fineract/test/data/TransactionType.java | 3 +-
...mortizationTransactionCreatedBusinessEvent.java | 34 ++-----
.../fineract/test/stepdef/loan/LoanStepDef.java | 19 ++++
.../features/LoanCapitalizedIncome.feature | 102 +++++++++++++++++++++
...mortizationTransactionCreatedBusinessEvent.java | 40 +++-----
.../api/LoanTransactionApiConstants.java | 1 +
.../loanaccount/data/AccountingBridgeDataDTO.java | 1 +
.../loanaccount/data/LoanTransactionEnumData.java | 2 +
.../loanaccount/domain/LoanTransaction.java | 13 ++-
.../loanaccount/domain/LoanTransactionType.java | 2 +
.../mapper/LoanAccountingBridgeMapper.java | 9 +-
...talizedIncomeAmortizationProcessingService.java | 32 +------
.../loanproduct/service/LoanEnumerations.java | 3 +
.../accounting/journalentry/data/LoanDTO.java | 1 +
.../service/AccountingProcessorHelper.java | 3 +-
.../AccrualBasedAccountingProcessorForLoan.java | 53 ++++++++++-
.../api/LoanTransactionsApiResource.java | 1 +
.../loanaccount/api/LoansApiResourceSwagger.java | 4 +
.../service/LoanAccrualsProcessingServiceImpl.java | 2 +-
...nCapitalizedIncomeAmortizationEventService.java | 71 ++++++++++++++
...zedIncomeAmortizationProcessingServiceImpl.java | 80 ++++++++++++++++
.../starter/LoanAccountConfiguration.java | 11 +++
.../db/changelog/tenant/changelog-tenant.xml | 1 +
...capitalized_income_amortization_transaction.xml | 38 ++++++++
.../CreateJournalEntriesForChargeOffLoanTest.java | 2 +-
...nalEventConfigurationValidationServiceTest.java | 6 +-
.../common/ExternalEventConfigurationHelper.java | 6 ++
27 files changed, 447 insertions(+), 93 deletions(-)
diff --git
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/TransactionType.java
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/TransactionType.java
index 5a0f75a217..950e2d37ea 100644
---
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/TransactionType.java
+++
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/data/TransactionType.java
@@ -34,7 +34,8 @@ public enum TransactionType {
CHARGE_ADJUSTMENT("chargeAdjustment"), //
INTEREST_PAYMENT_WAIVER("interestPaymentWaiver"), //
REPAYMENT_AT_DISBURSEMENT("repaymentAtDisbursement"), //
- CAPITALIZED_INCOME("capitalizedIncome"),;
+ CAPITALIZED_INCOME("capitalizedIncome"), //
+ CAPITALIZED_INCOME_AMORTIZATION("capitalizedIncomeAmortization"),;
public final String value;
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/loan/transaction/LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent.java
old mode 100755
new mode 100644
similarity index 50%
copy from
fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
copy to
fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/loan/transaction/LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent.java
index 8b7fd89335..ad0c08b138
---
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
+++
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/messaging/event/loan/transaction/LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent.java
@@ -16,34 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.accounting.journalentry.data;
+package org.apache.fineract.test.messaging.event.loan.transaction;
-import java.util.List;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.Setter;
+public class LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent
extends AbstractLoanTransactionEvent {
-@AllArgsConstructor
-@Getter
-public class LoanDTO {
-
- @Setter
- private Long loanId;
- @Setter
- private Long loanProductId;
- @Setter
- private Long officeId;
- @Setter
- private String currencyCode;
- @Setter
- private boolean cashBasedAccountingEnabled;
- private final boolean upfrontAccrualBasedAccountingEnabled;
- private final boolean periodicAccrualBasedAccountingEnabled;
- @Setter
- private List<LoanTransactionDTO> newLoanTransactions;
- @Setter
- private boolean markedAsChargeOff;
- @Setter
- private boolean markedAsFraud;
- private Long chargeOffReasonCodeValue;
+ @Override
+ public String getEventName() {
+ return
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent";
+ }
}
diff --git
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java
index 375c43cc63..84a70944e2 100644
---
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java
+++
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanStepDef.java
@@ -139,6 +139,7 @@ import
org.apache.fineract.test.messaging.event.loan.transaction.BulkBusinessEve
import
org.apache.fineract.test.messaging.event.loan.transaction.LoanAccrualAdjustmentTransactionBusinessEvent;
import
org.apache.fineract.test.messaging.event.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
import
org.apache.fineract.test.messaging.event.loan.transaction.LoanAdjustTransactionBusinessEvent;
+import
org.apache.fineract.test.messaging.event.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent;
import
org.apache.fineract.test.messaging.event.loan.transaction.LoanChargeAdjustmentPostBusinessEvent;
import
org.apache.fineract.test.messaging.event.loan.transaction.LoanChargeOffEvent;
import
org.apache.fineract.test.messaging.event.loan.transaction.LoanChargeOffUndoEvent;
@@ -4116,4 +4117,22 @@ public class LoanStepDef extends AbstractStepDef {
.isEqualTo(ErrorMessageHelper.addCapitalizedIncomeExceedApprovedAmountFailure());
}
+ @Then("LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent is
raised on {string}")
+ public void
checkLoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(final
String date) throws IOException {
+ Response<PostLoansResponse> loanCreateResponse =
testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
+ long loanId = loanCreateResponse.body().getLoanId();
+
+ Response<GetLoansLoanIdResponse> loanDetailsResponse =
loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
+ ErrorHelper.checkSuccessfulApiCall(loanDetailsResponse);
+
+ List<GetLoansLoanIdTransactions> transactions =
loanDetailsResponse.body().getTransactions();
+ GetLoansLoanIdTransactions finalAmortizationTransaction =
transactions.stream()
+ .filter(t -> date.equals(FORMATTER.format(t.getDate())) &&
"Capitalized Income Amortization".equals(t.getType().getValue()))
+ .findFirst().orElseThrow(
+ () -> new IllegalStateException(String.format("No
Capitalized Income Amortization transaction found on %s", date)));
+ Long finalAmortizationTransactionId =
finalAmortizationTransaction.getId();
+
+
eventAssertion.assertEventRaised(LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent.class,
+ finalAmortizationTransactionId);
+ }
}
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 89ae9f7e22..6d9d790625 100644
---
a/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature
+++
b/fineract-e2e-tests-runner/src/test/resources/features/LoanCapitalizedIncome.feature
@@ -167,3 +167,105 @@ Feature: Capitalized Income
| Type | Account code | Account name | Debit |
Credit |
| ASSET | 112601 | Loans Receivable | 300.0 |
|
| LIABILITY | 145024 | Deferred Capitalized Income | |
300.0 |
+
+ @TestRailId:C3646
+ Scenario: As a user I want to add capitalized income to a progressive loan
after disbursement and then make a full repayment - amortization in case of
loan close event
+ When Admin sets the business date to "1 January 2024"
+ And Admin creates a client with random data
+ When Admin creates a fully customized loan with the following data:
+ | LoanProduct
| submitted on date | with Principal | ANNUAL interest rate % |
interest type | interest calculation period | amortization type |
loanTermFrequency | loanTermFrequencyType | repaymentEvery |
repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment |
graceOnInterestPayment | interest free period | Payment strategy |
+ |
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME
| 01 January 2024 | 1000 | 7 |
DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 1
| MONTHS | 1 | MONTHS | 1
| 0 | 0 | 0
| ADVANCED_PAYMENT_ALLOCATION |
+ And Admin successfully approves the loan on "1 January 2024" with "1000"
amount and expected disbursement date on "1 January 2024"
+ And Admin successfully disburse the loan on "1 January 2024" with "900"
EUR transaction amount
+ Then Loan status will be "ACTIVE"
+ When Admin sets the business date to "2 January 2024"
+ And Admin adds capitalized income with "AUTOPAY" payment type to the loan
on "2 January 2024" with "100" EUR transaction amount
+ 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 |
+ Then Loan Transactions tab has a "CAPITALIZED_INCOME" transaction with
date "02 January 2024" which has the following Journal entries:
+ | 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
+ 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 | 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:
+ | Type | Account code | Account name | Debit |
Credit |
+ | INCOME | 404000 | Interest Income | |
100.0 |
+ | LIABILITY | 145024 | Deferred Capitalized Income | 100.0 |
|
+ Then LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent is
raised on "02 January 2024"
+
+ @TestRailId:C3647
+ Scenario: As a user I want to add capitalized income to a progressive loan
after disbursement and then overpay loan - amortization in case of loan close
event
+ When Admin sets the business date to "1 January 2024"
+ And Admin creates a client with random data
+ When Admin creates a fully customized loan with the following data:
+ | LoanProduct
| submitted on date | with Principal | ANNUAL interest rate % |
interest type | interest calculation period | amortization type |
loanTermFrequency | loanTermFrequencyType | repaymentEvery |
repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment |
graceOnInterestPayment | interest free period | Payment strategy |
+ |
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME
| 01 January 2024 | 1000 | 7 |
DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 1
| MONTHS | 1 | MONTHS | 1
| 0 | 0 | 0
| ADVANCED_PAYMENT_ALLOCATION |
+ And Admin successfully approves the loan on "1 January 2024" with "1000"
amount and expected disbursement date on "1 January 2024"
+ And Admin successfully disburse the loan on "1 January 2024" with "900"
EUR transaction amount
+ Then Loan status will be "ACTIVE"
+ When Admin sets the business date to "2 January 2024"
+ And Admin adds capitalized income with "AUTOPAY" payment type to the loan
on "2 January 2024" with "100" EUR transaction amount
+ 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 |
+ Then Loan Transactions tab has a "CAPITALIZED_INCOME" transaction with
date "02 January 2024" which has the following Journal entries:
+ | 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
+ 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 | 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:
+ | Type | Account code | Account name | Debit |
Credit |
+ | INCOME | 404000 | Interest Income | |
100.0 |
+ | LIABILITY | 145024 | Deferred Capitalized Income | 100.0 |
|
+ Then LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent is
raised on "02 January 2024"
+
+ @TestRailId:C3651
+ Scenario: As a user I want to add capitalized income to a progressive loan
after disbursement and then write off loan - amortization in case of loan close
event
+ When Admin sets the business date to "1 January 2024"
+ And Admin creates a client with random data
+ When Admin creates a fully customized loan with the following data:
+ | LoanProduct
| submitted on date | with Principal | ANNUAL interest rate % |
interest type | interest calculation period | amortization type |
loanTermFrequency | loanTermFrequencyType | repaymentEvery |
repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment |
graceOnInterestPayment | interest free period | Payment strategy |
+ |
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME
| 01 January 2024 | 1000 | 7 |
DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 1
| MONTHS | 1 | MONTHS | 1
| 0 | 0 | 0
| ADVANCED_PAYMENT_ALLOCATION |
+ And Admin successfully approves the loan on "1 January 2024" with "1000"
amount and expected disbursement date on "1 January 2024"
+ And Admin successfully disburse the loan on "1 January 2024" with "900"
EUR transaction amount
+ Then Loan status will be "ACTIVE"
+ When Admin sets the business date to "2 January 2024"
+ And Admin adds capitalized income with "AUTOPAY" payment type to the loan
on "2 January 2024" with "100" EUR transaction amount
+ 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 |
+ Then Loan Transactions tab has a "CAPITALIZED_INCOME" transaction with
date "02 January 2024" which has the following Journal entries:
+ | Type | Account code | Account name | Debit |
Credit |
+ | ASSET | 112601 | Loans Receivable | 100.0 |
|
+ | LIABILITY | 145024 | Deferred Capitalized Income | |
100.0 |
+ And Admin does write-off the loan on "02 January 2024"
+ Then Loan status will be "CLOSED_WRITTEN_OFF"
+ 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 | Close (as written-off) | 905.25 | 900.0
| 5.25 | 0.0 | 0.0 | 100.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 |
+ | EXPENSE | e4 | Written off | |
100.0 |
+ | LIABILITY | 145024 | Deferred Capitalized Income | 100.0 |
|
+ Then LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent is
raised on "02 January 2024"
\ No newline at end of file
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent.java
old mode 100755
new mode 100644
similarity index 51%
copy from
fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
copy to
fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent.java
index 8b7fd89335..d76d69733c
---
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/infrastructure/event/business/domain/loan/transaction/LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent.java
@@ -16,34 +16,20 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.accounting.journalentry.data;
+package
org.apache.fineract.infrastructure.event.business.domain.loan.transaction;
-import java.util.List;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.Setter;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
-@AllArgsConstructor
-@Getter
-public class LoanDTO {
+public class LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent
extends LoanTransactionBusinessEvent {
- @Setter
- private Long loanId;
- @Setter
- private Long loanProductId;
- @Setter
- private Long officeId;
- @Setter
- private String currencyCode;
- @Setter
- private boolean cashBasedAccountingEnabled;
- private final boolean upfrontAccrualBasedAccountingEnabled;
- private final boolean periodicAccrualBasedAccountingEnabled;
- @Setter
- private List<LoanTransactionDTO> newLoanTransactions;
- @Setter
- private boolean markedAsChargeOff;
- @Setter
- private boolean markedAsFraud;
- private Long chargeOffReasonCodeValue;
+ private static final String TYPE =
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent";
+
+ public
LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(LoanTransaction
value) {
+ super(value);
+ }
+
+ @Override
+ public String getType() {
+ return TYPE;
+ }
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionApiConstants.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionApiConstants.java
index bb0de9cf81..70de654919 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionApiConstants.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionApiConstants.java
@@ -53,5 +53,6 @@ public interface LoanTransactionApiConstants {
interestRefund, //
accrualAdjustment, //
capitalizedIncome, //
+ capitalizedIncomeAmortization, //
}
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccountingBridgeDataDTO.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccountingBridgeDataDTO.java
index 86bcf7ef75..784136c05d 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccountingBridgeDataDTO.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/AccountingBridgeDataDTO.java
@@ -44,6 +44,7 @@ public class AccountingBridgeDataDTO {
private boolean isChargeOff;
private boolean isFraud;
private Long chargeOffReasonCodeValue;
+ private boolean isWrittenOff;
private List<AccountingBridgeLoanTransactionDTO> newLoanTransactions = new
ArrayList<>();
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
index bc0aa9a307..573b45703b 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
@@ -68,6 +68,7 @@ public class LoanTransactionEnumData implements Serializable {
private final boolean interestRefund;
private final boolean accrualAdjustment;
private final boolean capitalizedIncome;
+ private final boolean capitalizedIncomeAmortization;
public LoanTransactionEnumData(final Long id, final String code, final
String value) {
this.id = id;
@@ -105,6 +106,7 @@ public class LoanTransactionEnumData implements
Serializable {
this.interestRefund =
Long.valueOf(LoanTransactionType.INTEREST_REFUND.getValue()).equals(this.id);
this.accrualAdjustment =
Long.valueOf(LoanTransactionType.ACCRUAL_ADJUSTMENT.getValue()).equals(this.id);
this.capitalizedIncome =
Long.valueOf(LoanTransactionType.CAPITALIZED_INCOME.getValue()).equals(this.id);
+ this.capitalizedIncomeAmortization =
Long.valueOf(LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION.getValue()).equals(this.id);
}
public boolean isRepaymentType() {
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
index cbd74830ba..dc34a34938 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
@@ -312,6 +312,16 @@ public class LoanTransaction extends
AbstractAuditableWithUTCDateTimeCustom<Long
amount.getAmount(), null, null, null, null, false,
paymentDetail, externalId);
}
+ public static LoanTransaction capitalizedIncomeAmortization(final Loan
loan, final Office office, final LocalDate dateOf,
+ final BigDecimal amount, final ExternalId externalId) {
+ return switch
(loan.getLoanProductRelatedDetail().getCapitalizedIncomeType()) {
+ case FEE -> new LoanTransaction(loan, office,
LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION, dateOf, amount, null, null,
+ amount, null, null, false, null, externalId);
+ case INTEREST -> new LoanTransaction(loan, office,
LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION, dateOf, amount, null,
+ amount, null, null, null, false, null, externalId);
+ };
+ }
+
public LoanTransaction copyTransactionPropertiesAndMappings() {
LoanTransaction newTransaction = copyTransactionProperties(this);
newTransaction.updateLoanTransactionToRepaymentScheduleMappings(loanTransactionToRepaymentScheduleMappings);
@@ -746,7 +756,8 @@ public class LoanTransaction extends
AbstractAuditableWithUTCDateTimeCustom<Long
|| type == LoanTransactionType.ACCRUAL_ACTIVITY || type ==
LoanTransactionType.APPROVE_TRANSFER
|| type == LoanTransactionType.INITIATE_TRANSFER || type ==
LoanTransactionType.REJECT_TRANSFER
|| type == LoanTransactionType.WITHDRAW_TRANSFER || type ==
LoanTransactionType.CHARGE_OFF
- || type == LoanTransactionType.REAMORTIZE || type ==
LoanTransactionType.REAGE);
+ || type == LoanTransactionType.REAMORTIZE || type ==
LoanTransactionType.REAGE
+ || type ==
LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION);
}
public void updateOutstandingLoanBalance(BigDecimal
outstandingLoanBalance) {
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
index 709d3cf0ec..195b4e4363 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionType.java
@@ -68,6 +68,7 @@ public enum LoanTransactionType {
INTEREST_REFUND(33, "loanTransactionType.interestRefund"), //
ACCRUAL_ADJUSTMENT(34, "loanTransactionType.accrualAdjustment"), //
CAPITALIZED_INCOME(35, "loanTransactionType.capitalizedIncome"), //
+ CAPITALIZED_INCOME_AMORTIZATION(36,
"loanTransactionType.capitalizedIncomeAmortization"), //
;
private final Integer value;
@@ -119,6 +120,7 @@ public enum LoanTransactionType {
case 33 -> LoanTransactionType.INTEREST_REFUND;
case 34 -> LoanTransactionType.ACCRUAL_ADJUSTMENT;
case 35 -> LoanTransactionType.CAPITALIZED_INCOME;
+ case 36 -> LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION;
default -> LoanTransactionType.INVALID;
};
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanAccountingBridgeMapper.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanAccountingBridgeMapper.java
index c2d3916b97..f840e5babe 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanAccountingBridgeMapper.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/mapper/LoanAccountingBridgeMapper.java
@@ -55,12 +55,14 @@ public class LoanAccountingBridgeMapper {
AccountingBridgeDataDTO beforeChargeOff = new
AccountingBridgeDataDTO(loan.getId(), loan.productId(), loan.getOfficeId(),
currencyCode, loan.getSummary().getTotalInterestCharged(),
loan.isCashBasedAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(),
loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(),
- isAccountTransfer, false, loan.isFraud(),
loan.fetchChargeOffReasonId(), newLoanTransactionsBeforeChargeOff);
+ isAccountTransfer, false, loan.isFraud(),
loan.fetchChargeOffReasonId(), loan.isClosedWrittenOff(),
+ newLoanTransactionsBeforeChargeOff);
AccountingBridgeDataDTO afterChargeOff = new
AccountingBridgeDataDTO(loan.getId(), loan.productId(), loan.getOfficeId(),
currencyCode, loan.getSummary().getTotalInterestCharged(),
loan.isCashBasedAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(),
loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(),
- isAccountTransfer, true, loan.isFraud(),
loan.fetchChargeOffReasonId(), newLoanTransactionsAfterChargeOff);
+ isAccountTransfer, true, loan.isFraud(),
loan.fetchChargeOffReasonId(), loan.isClosedWrittenOff(),
+ newLoanTransactionsAfterChargeOff);
List<AccountingBridgeDataDTO> result = new ArrayList<>();
result.add(beforeChargeOff);
@@ -83,7 +85,8 @@ public class LoanAccountingBridgeMapper {
return new AccountingBridgeDataDTO(loan.getId(), loan.productId(),
loan.getOfficeId(), currencyCode,
loan.getSummary().getTotalInterestCharged(),
loan.isCashBasedAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(),
loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(),
- isAccountTransfer, loan.isChargedOff(), loan.isFraud(),
loan.fetchChargeOffReasonId(), newLoanTransactions);
+ isAccountTransfer, loan.isChargedOff(), loan.isFraud(),
loan.fetchChargeOffReasonId(), loan.isClosedWrittenOff(),
+ newLoanTransactions);
}
public AccountingBridgeLoanTransactionDTO mapToLoanTransactionData(final
LoanTransaction transaction, final String currencyCode) {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingService.java
old mode 100755
new mode 100644
similarity index 51%
copy from
fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
copy to
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingService.java
index 8b7fd89335..0567a3fa6c
---
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingService.java
@@ -16,34 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.accounting.journalentry.data;
+package org.apache.fineract.portfolio.loanaccount.service;
-import java.util.List;
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.Setter;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.springframework.lang.NonNull;
-@AllArgsConstructor
-@Getter
-public class LoanDTO {
+public interface LoanCapitalizedIncomeAmortizationProcessingService {
- @Setter
- private Long loanId;
- @Setter
- private Long loanProductId;
- @Setter
- private Long officeId;
- @Setter
- private String currencyCode;
- @Setter
- private boolean cashBasedAccountingEnabled;
- private final boolean upfrontAccrualBasedAccountingEnabled;
- private final boolean periodicAccrualBasedAccountingEnabled;
- @Setter
- private List<LoanTransactionDTO> newLoanTransactions;
- @Setter
- private boolean markedAsChargeOff;
- @Setter
- private boolean markedAsFraud;
- private Long chargeOffReasonCodeValue;
+ void processCapitalizedIncomeAmortizationOnLoanClosure(@NonNull Loan loan);
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
index 9093c8642b..f3c77f5725 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/service/LoanEnumerations.java
@@ -327,6 +327,9 @@ public final class LoanEnumerations {
LoanTransactionType.ACCRUAL_ADJUSTMENT.getCode(), "Accrual
Adjustment");
case CAPITALIZED_INCOME -> new
LoanTransactionEnumData(LoanTransactionType.CAPITALIZED_INCOME.getValue().longValue(),
LoanTransactionType.CAPITALIZED_INCOME.getCode(),
"Capitalized Income");
+ case CAPITALIZED_INCOME_AMORTIZATION ->
+ new
LoanTransactionEnumData(LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION.getValue().longValue(),
+
LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION.getCode(), "Capitalized
Income Amortization");
};
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
index 8b7fd89335..abe9389f94 100755
---
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/data/LoanDTO.java
@@ -46,4 +46,5 @@ public class LoanDTO {
@Setter
private boolean markedAsFraud;
private Long chargeOffReasonCodeValue;
+ private boolean markedAsWrittenOff;
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
index b4e26d4484..300ba5e47d 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccountingProcessorHelper.java
@@ -107,6 +107,7 @@ public class AccountingProcessorHelper {
boolean isLoanMarkedAsChargeOff = accountingBridgeData.isChargeOff();
boolean isLoanMarkedAsFraud = accountingBridgeData.isFraud();
final Long chargeOffReasonCodeValue =
accountingBridgeData.getChargeOffReasonCodeValue();
+ final boolean isLoanMarkedAsWrittenOff =
accountingBridgeData.isWrittenOff();
final boolean cashBasedAccountingEnabled =
accountingBridgeData.isCashBasedAccountingEnabled();
final boolean upfrontAccrualBasedAccountingEnabled =
accountingBridgeData.isUpfrontAccrualBasedAccountingEnabled();
final boolean periodicAccrualBasedAccountingEnabled =
accountingBridgeData.isPeriodicAccrualBasedAccountingEnabled();
@@ -169,7 +170,7 @@ public class AccountingProcessorHelper {
return new LoanDTO(loanId, loanProductId, officeId, currencyCode,
cashBasedAccountingEnabled, upfrontAccrualBasedAccountingEnabled,
periodicAccrualBasedAccountingEnabled, newLoanTransactions,
isLoanMarkedAsChargeOff, isLoanMarkedAsFraud,
- chargeOffReasonCodeValue);
+ chargeOffReasonCodeValue, isLoanMarkedAsWrittenOff);
}
public ProductToGLAccountMapping getChargeOffMappingByCodeValue(Long
loanProductId, PortfolioProductType productType,
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
index 722966e21d..cb6a759f96 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
@@ -127,6 +127,10 @@ public class AccrualBasedAccountingProcessorForLoan
implements AccountingProcess
if (transactionType.isCapitalizedIncome()) {
createJournalEntriesForCapitalizedIncome(loanDTO,
loanTransactionDTO, office);
}
+ // Handle Capitalized Income Amortization
+ if (transactionType.isCapitalizedIncomeAmortization()) {
+ createJournalEntriesForCapitalizedIncomeAmortization(loanDTO,
loanTransactionDTO, office);
+ }
}
}
@@ -141,7 +145,7 @@ public class AccrualBasedAccountingProcessorForLoan
implements AccountingProcess
final LocalDate transactionDate =
loanTransactionDTO.getTransactionDate();
final BigDecimal principalAmount = loanTransactionDTO.getPrincipal();
final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
- GLAccountBalanceHolder glAccountBalanceHolder = new
GLAccountBalanceHolder();
+ final GLAccountBalanceHolder glAccountBalanceHolder = new
GLAccountBalanceHolder();
if (MathUtil.isGreaterThanZero(principalAmount)) {
populateCreditDebitMaps(loanProductId, principalAmount,
paymentTypeId,
@@ -167,6 +171,53 @@ public class AccrualBasedAccountingProcessorForLoan
implements AccountingProcess
}
}
+ private void createJournalEntriesForCapitalizedIncomeAmortization(final
LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
+ final Office office) {
+ // loan properties
+ final Long loanProductId = loanDTO.getLoanProductId();
+ final Long loanId = loanDTO.getLoanId();
+ final String currencyCode = loanDTO.getCurrencyCode();
+ final boolean isLoanWrittenOff = loanDTO.isMarkedAsWrittenOff();
+
+ // transaction properties
+ final String transactionId = loanTransactionDTO.getTransactionId();
+ final LocalDate transactionDate =
loanTransactionDTO.getTransactionDate();
+ final BigDecimal interestAmount = loanTransactionDTO.getInterest();
+ final BigDecimal feesAmount = loanTransactionDTO.getFees();
+ final Long paymentTypeId = loanTransactionDTO.getPaymentTypeId();
+ final GLAccountBalanceHolder glAccountBalanceHolder = new
GLAccountBalanceHolder();
+
+ // interest payment
+ final AccrualAccountsForLoan creditAccountType = isLoanWrittenOff ?
AccrualAccountsForLoan.LOSSES_WRITTEN_OFF
+ : AccrualAccountsForLoan.INCOME_FROM_CAPITALIZATION;
+ if (MathUtil.isGreaterThanZero(interestAmount)) {
+ populateCreditDebitMaps(loanProductId, interestAmount,
paymentTypeId, creditAccountType.getValue(),
+
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(),
glAccountBalanceHolder);
+ }
+ // handle fees payment
+ if (MathUtil.isGreaterThanZero(feesAmount)) {
+ populateCreditDebitMaps(loanProductId, feesAmount, paymentTypeId,
creditAccountType.getValue(),
+
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(),
glAccountBalanceHolder);
+ }
+
+ // create credit entries
+ for (Map.Entry<Long, BigDecimal> creditEntry :
glAccountBalanceHolder.getCreditBalances().entrySet()) {
+ if (MathUtil.isGreaterThanZero(creditEntry.getValue())) {
+ GLAccount glAccount =
glAccountBalanceHolder.getGlAccountMap().get(creditEntry.getKey());
+ this.helper.createCreditJournalEntryForLoan(office,
currencyCode, loanId, transactionId, transactionDate,
+ creditEntry.getValue(), glAccount);
+ }
+ }
+ // create debit entries
+ for (Map.Entry<Long, BigDecimal> debitEntry :
glAccountBalanceHolder.getDebitBalances().entrySet()) {
+ if (MathUtil.isGreaterThanZero(debitEntry.getValue())) {
+ GLAccount glAccount =
glAccountBalanceHolder.getGlAccountMap().get(debitEntry.getKey());
+ this.helper.createDebitJournalEntryForLoan(office,
currencyCode, loanId, transactionId, transactionDate,
+ debitEntry.getValue(), glAccount);
+ }
+ }
+ }
+
private void
createJournalEntriesForInterestPaymentWaiverOrInterestRefund(LoanDTO loanDTO,
LoanTransactionDTO loanTransactionDTO,
Office office) {
final Long loanProductId = loanDTO.getLoanProductId();
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
index 2a410513e6..68dbc254be 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanTransactionsApiResource.java
@@ -528,6 +528,7 @@ public class LoanTransactionsApiResource {
case interestRefund -> LoanTransactionType.INTEREST_REFUND;
case accrualAdjustment -> LoanTransactionType.ACCRUAL_ADJUSTMENT;
case capitalizedIncome -> LoanTransactionType.CAPITALIZED_INCOME;
+ case capitalizedIncomeAmortization ->
LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION;
default ->
throw new InvalidLoanTransactionTypeException("transaction",
transactionTypeParam.name(), "Unknown transaction type");
};
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 0b146908aa..c3d053d073 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
@@ -722,6 +722,10 @@ final class LoansApiResourceSwagger {
public boolean chargeAdjustment;
@Schema(example = "false")
public boolean chargeoff;
+ @Schema(example = "false")
+ public boolean capitalizedIncome;
+ @Schema(example = "false")
+ public boolean capitalizedIncomeAmortization;
}
static final class GetLoansLoanIdPaymentDetailData {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java
index a423262210..6dd45dceca 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java
@@ -440,7 +440,7 @@ public class LoanAccrualsProcessingServiceImpl implements
LoanAccrualsProcessing
loan.getOfficeId(), loan.getCurrencyCode(),
loan.getSummary().getTotalInterestCharged(),
loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct(),
loan.isUpfrontAccrualAccountingEnabledOnLoanProduct(),
loan.isPeriodicAccrualAccountingEnabledOnLoanProduct(), false,
- false, false, null, newTransactionDTOs);
+ false, false, null, false, newTransactionDTOs);
this.journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
}
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationEventService.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationEventService.java
new file mode 100644
index 0000000000..af9cf3a3f0
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationEventService.java
@@ -0,0 +1,71 @@
+/**
+ * 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 jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.event.business.BusinessEventListener;
+import
org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
+import
org.apache.fineract.infrastructure.event.business.domain.loan.LoanCloseBusinessEvent;
+import
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
+
+@Slf4j
+@RequiredArgsConstructor
+public class LoanCapitalizedIncomeAmortizationEventService {
+
+ private final BusinessEventNotifierService businessEventNotifierService;
+ private final LoanCapitalizedIncomeAmortizationProcessingService
loanCapitalizedIncomeAmortizationProcessingService;
+
+ @PostConstruct
+ public void addListeners() {
+
businessEventNotifierService.addPreBusinessEventListener(LoanCloseBusinessEvent.class,
new LoanCloseListener());
+
businessEventNotifierService.addPostBusinessEventListener(LoanBalanceChangedBusinessEvent.class,
new LoanBalanceChangedListener());
+ }
+
+ private final class LoanCloseListener implements
BusinessEventListener<LoanCloseBusinessEvent> {
+
+ @Override
+ public void onBusinessEvent(final LoanCloseBusinessEvent event) {
+ final Loan loan = event.get();
+ final LoanStatus status = loan.getStatus();
+ if
(loan.getLoanProductRelatedDetail().isEnableIncomeCapitalization()
+ && (status.isClosedObligationsMet() ||
status.isClosedWrittenOff() || status.isOverpaid())) {
+ log.debug("Loan closure on capitalized income amortization for
loan {}", loan.getId());
+
loanCapitalizedIncomeAmortizationProcessingService.processCapitalizedIncomeAmortizationOnLoanClosure(loan);
+ }
+ }
+ }
+
+ private final class LoanBalanceChangedListener implements
BusinessEventListener<LoanBalanceChangedBusinessEvent> {
+
+ @Override
+ public void onBusinessEvent(final LoanBalanceChangedBusinessEvent
event) {
+ final Loan loan = event.get();
+ final LoanStatus status = loan.getStatus();
+ if
(loan.getLoanProductRelatedDetail().isEnableIncomeCapitalization()
+ && (status.isClosedObligationsMet() ||
status.isClosedWrittenOff() || status.isOverpaid())) {
+ log.debug("Loan balance change on capitalized income
amortization for loan {}", loan.getId());
+
loanCapitalizedIncomeAmortizationProcessingService.processCapitalizedIncomeAmortizationOnLoanClosure(loan);
+ }
+ }
+ }
+}
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
new file mode 100644
index 0000000000..9feba20c57
--- /dev/null
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanCapitalizedIncomeAmortizationProcessingServiceImpl.java
@@ -0,0 +1,80 @@
+/**
+ * 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 java.time.LocalDate;
+import lombok.RequiredArgsConstructor;
+import
org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import
org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent;
+import
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import
org.apache.fineract.portfolio.loanaccount.domain.LoanCapitalizedIncomeBalance;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
+import
org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+@RequiredArgsConstructor
+public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements
LoanCapitalizedIncomeAmortizationProcessingService {
+
+ private final ConfigurationDomainService configurationDomainService;
+ private final LoanTransactionRepository loanTransactionRepository;
+ private final LoanCapitalizedIncomeBalanceRepository
loanCapitalizedIncomeBalanceRepository;
+ private final BusinessEventNotifierService businessEventNotifierService;
+
+ @Override
+ @Transactional
+ public void processCapitalizedIncomeAmortizationOnLoanClosure(@NotNull
final Loan loan) {
+ final LocalDate finalAmortizationTransactionDate =
getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
+ ExternalId externalId = ExternalId.empty();
+ BigDecimal totalUnrecognizedAmountCapitalizedIncomeAmount =
BigDecimal.ZERO;
+
+ final boolean isExternalIdAutoGenerationEnabled =
configurationDomainService.isExternalIdAutoGenerationEnabled();
+ if (isExternalIdAutoGenerationEnabled) {
+ externalId = ExternalId.generate();
+ }
+
+ for (LoanCapitalizedIncomeBalance balance :
loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId())) {
+ totalUnrecognizedAmountCapitalizedIncomeAmount =
totalUnrecognizedAmountCapitalizedIncomeAmount
+ .add(balance.getUnrecognizedAmount());
+ balance.setUnrecognizedAmount(BigDecimal.ZERO);
+ }
+
+ final LoanTransaction finalCapitalizedIncomeAmortization =
LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(),
+ finalAmortizationTransactionDate,
totalUnrecognizedAmountCapitalizedIncomeAmount, externalId);
+ loan.addLoanTransaction(finalCapitalizedIncomeAmortization);
+
loanTransactionRepository.saveAndFlush(finalCapitalizedIncomeAmortization);
+ businessEventNotifierService.notifyPostBusinessEvent(
+ new
LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(finalCapitalizedIncomeAmortization));
+ }
+
+ private LocalDate
getFinalCapitalizedIncomeAmortizationTransactionDate(final Loan loan) {
+ return switch (loan.getStatus()) {
+ case CLOSED_OBLIGATIONS_MET -> loan.getClosedOnDate();
+ case OVERPAID -> loan.getOverpaidOnDate();
+ case CLOSED_WRITTEN_OFF -> loan.getWrittenOffOnDate();
+ default -> throw new IllegalStateException("Unexpected value: " +
loan.getStatus());
+ };
+ }
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
index 10f65716c0..2d4b51cf59 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
@@ -119,6 +119,8 @@ import
org.apache.fineract.portfolio.loanaccount.service.LoanArrearsAgingService
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssemblerImpl;
import
org.apache.fineract.portfolio.loanaccount.service.LoanCalculateRepaymentPastDueService;
+import
org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationEventService;
+import
org.apache.fineract.portfolio.loanaccount.service.LoanCapitalizedIncomeAmortizationProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler;
import
org.apache.fineract.portfolio.loanaccount.service.LoanChargePaidByReadService;
import
org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService;
@@ -505,4 +507,13 @@ public class LoanAccountConfiguration {
return new LoanAccountServiceImpl(loanRepositoryWrapper,
loanTransactionRepository);
}
+ @Bean
+
@ConditionalOnMissingBean(LoanCapitalizedIncomeAmortizationEventService.class)
+ public LoanCapitalizedIncomeAmortizationEventService
loanCapitalizedIncomeAmortizationEventService(
+ BusinessEventNotifierService businessEventNotifierService,
+ LoanCapitalizedIncomeAmortizationProcessingService
loanCapitalizedIncomeAmortizationProcessingService) {
+ return new
LoanCapitalizedIncomeAmortizationEventService(businessEventNotifierService,
+ loanCapitalizedIncomeAmortizationProcessingService);
+ }
+
}
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 09122d942f..bfb36e98fc 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
@@ -194,4 +194,5 @@
<include file="parts/0173_user_change_pwd.xml"
relativeToChangelogFile="true" />
<include file="parts/0174_loan_product_add_capitalized_income_type.xml"
relativeToChangelogFile="true" />
<include file="parts/0175_add_fk_acc_product_mapping.xml"
relativeToChangelogFile="true" />
+ <include
file="parts/0176_add_capitalized_income_amortization_transaction.xml"
relativeToChangelogFile="true" />
</databaseChangeLog>
diff --git
a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0176_add_capitalized_income_amortization_transaction.xml
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0176_add_capitalized_income_amortization_transaction.xml
new file mode 100644
index 0000000000..7b00050710
--- /dev/null
+++
b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0176_add_capitalized_income_amortization_transaction.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+
+-->
+<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
+ <changeSet id="1" author="fineract">
+ <insert tableName="m_external_event_configuration">
+ <column name="type"
value="LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent"/>
+ <column name="enabled" valueBoolean="false"/>
+ </insert>
+ <insert tableName="r_enum_value">
+ <column name="enum_name" value="transaction_type_enum"/>
+ <column name="enum_id" valueNumeric="36"/>
+ <column name="enum_message_property" value="Capitalized Income
Amortization"/>
+ <column name="enum_value" value="Capitalized Income Amortization"/>
+ <column name="enum_type" valueBoolean="false"/>
+ </insert>
+ </changeSet>
+</databaseChangeLog>
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java
b/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java
index 7280364f78..1e8ddf379f 100644
---
a/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java
+++
b/fineract-provider/src/test/java/org/apache/fineract/accounting/journalentry/CreateJournalEntriesForChargeOffLoanTest.java
@@ -72,7 +72,7 @@ class CreateJournalEntriesForChargeOffLoanTest {
transactionType, new BigDecimal("500.00"), new
BigDecimal("500.00"), null, null, null, null, false, Collections.emptyList(),
Collections.emptyList(), false, "", null, null, null, null);
- loanDTO = new LoanDTO(1L, 1L, 1L, "USD", false, true, true,
List.of(loanTransactionDTO), false, false, chargeOffReasonId);
+ loanDTO = new LoanDTO(1L, 1L, 1L, "USD", false, true, true,
List.of(loanTransactionDTO), false, false, chargeOffReasonId, false);
}
@Test
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
index d1e501c589..00e59f0afd 100644
---
a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
+++
b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/event/external/service/ExternalEventConfigurationValidationServiceTest.java
@@ -105,7 +105,8 @@ public class
ExternalEventConfigurationValidationServiceTest {
"LoanReAmortizeBusinessEvent",
"LoanUndoReAmortizeBusinessEvent",
"LoanTransactionInterestPaymentWaiverPreBusinessEvent",
"LoanTransactionInterestPaymentWaiverPostBusinessEvent",
"LoanTransactionAccrualActivityPostBusinessEvent",
"LoanTransactionAccrualActivityPreBusinessEvent",
"LoanTransactionInterestRefundPostBusinessEvent",
- "LoanTransactionInterestRefundPreBusinessEvent",
"LoanAccrualAdjustmentTransactionBusinessEvent");
+ "LoanTransactionInterestRefundPreBusinessEvent",
"LoanAccrualAdjustmentTransactionBusinessEvent",
+
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default
Tenant", "Europe/Budapest", null));
@@ -191,7 +192,8 @@ public class
ExternalEventConfigurationValidationServiceTest {
"LoanReAmortizeBusinessEvent",
"LoanUndoReAmortizeBusinessEvent",
"LoanTransactionInterestPaymentWaiverPreBusinessEvent",
"LoanTransactionInterestPaymentWaiverPostBusinessEvent",
"LoanTransactionAccrualActivityPostBusinessEvent",
"LoanTransactionAccrualActivityPreBusinessEvent",
"LoanTransactionInterestRefundPostBusinessEvent",
- "LoanTransactionInterestRefundPreBusinessEvent",
"LoanAccrualAdjustmentTransactionBusinessEvent");
+ "LoanTransactionInterestRefundPreBusinessEvent",
"LoanAccrualAdjustmentTransactionBusinessEvent",
+
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent");
List<FineractPlatformTenant> tenants = Arrays
.asList(new FineractPlatformTenant(1L, "default", "Default
Tenant", "Europe/Budapest", null));
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
index 2a9cfc1320..03f91075df 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/ExternalEventConfigurationHelper.java
@@ -583,6 +583,12 @@ public class ExternalEventConfigurationHelper {
loanTransactionInterestRefundPreBusinessEvent.put("enabled", false);
defaults.add(loanTransactionInterestRefundPreBusinessEvent);
+ Map<String, Object>
loanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent = new
HashMap<>();
+
loanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent.put("type",
+
"LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent");
+
loanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent.put("enabled",
false);
+
defaults.add(loanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent);
+
return defaults;
}