FINERACT-2232: Capitalized income - amortization in case of Charge-off event
This commit is contained in:
committed by
Arnold Gálovics
parent
39062ad1a1
commit
4861ad19fd
+1
@@ -124,6 +124,7 @@ public enum DefaultLoanProduct implements LoanProduct {
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME_FEE, //
|
||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSAL_CAPITALIZED_INCOME, //
|
||||
LP2_ADV_PYMNT_ZERO_INTEREST_CHARGE_OFF_DELINQUENT_REASON_INTEREST_RECALC_CAPITALIZED_INCOME, //
|
||||
;
|
||||
|
||||
@Override
|
||||
|
||||
+11
@@ -1628,4 +1628,15 @@ public class LoanProductsRequestFactory {
|
||||
.incomeFromCapitalizationAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_INCOME));//
|
||||
}
|
||||
|
||||
public PostLoanProductsRequest defaultLoanProductsRequestLP2ChargeOffReasonToExpenseAccountMappingsWithCapitalizedIncome() {
|
||||
return defaultLoanProductsRequestLP2ChargeOffReasonToExpenseAccountMappings()//
|
||||
.enableIncomeCapitalization(true)//
|
||||
.enableAutoRepaymentForDownPayment(false)//
|
||||
.capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)//
|
||||
.capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)//
|
||||
.capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.INTEREST)//
|
||||
.deferredIncomeLiabilityAccountId(accountTypeResolver.resolve(DefaultAccountType.DEFERRED_CAPITALIZED_INCOME))//
|
||||
.incomeFromCapitalizationAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_INCOME));//
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
@@ -521,6 +521,13 @@ public final class ErrorMessageHelper {
|
||||
resourceId, line, actual, expected);
|
||||
}
|
||||
|
||||
public static String wrongValueInLineInJournalEntry(String resourceId, int line, List<List<String>> actualList, List<String> expected) {
|
||||
String actual = actualList.stream().map(Object::toString).collect(Collectors.joining(System.lineSeparator()));
|
||||
return String.format("%nWrong value in Journal entries of resource %s line %s." //
|
||||
+ "%nActual values for the possible transactions in line (with the same date) are: %n%s %nExpected values in line: %n%s",
|
||||
resourceId, line, actual, expected);
|
||||
}
|
||||
|
||||
public static String wrongDataInJournalEntriesGlAccountType(int line, String actual, String expected) {
|
||||
return String.format("Wrong data in Journal entries, line %s / GL account type. " //
|
||||
+ "Actual value is: %s - But expected value is: %s", line, actual, expected);
|
||||
|
||||
+30
@@ -2509,6 +2509,36 @@ public class LoanProductGlobalInitializerStep implements FineractGlobalInitializ
|
||||
TestContext.INSTANCE.set(
|
||||
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME_FEE,
|
||||
responseLoanProductsRequestLP2ProgressiveAdvPayment36030InterestRecalcCapitalizedIncomeFee);
|
||||
|
||||
// LP2 + zero-interest chargeOff behaviour + progressive loan schedule + horizontal + delinquent charge-off
|
||||
// reason to GL account mapping + interest recalculation
|
||||
// capitalized income enabled
|
||||
final String name102 = DefaultLoanProduct.LP2_ADV_PYMNT_ZERO_INTEREST_CHARGE_OFF_DELINQUENT_REASON_INTEREST_RECALC_CAPITALIZED_INCOME
|
||||
.getName();
|
||||
final PostLoanProductsRequest loanProductsRequestPrgAdvZeroIntChargeOffDelinquentReasonIntRecalcCapitalizedIncome = loanProductsRequestFactory
|
||||
.defaultLoanProductsRequestLP2ChargeOffReasonToExpenseAccountMappingsWithCapitalizedIncome()//
|
||||
.name(name102)//
|
||||
.isInterestRecalculationEnabled(true)//
|
||||
.preClosureInterestCalculationStrategy(1)//
|
||||
.rescheduleStrategyMethod(4)//
|
||||
.interestRecalculationCompoundingMethod(0)//
|
||||
.recalculationRestFrequencyType(2)//
|
||||
.recalculationRestFrequencyInterval(1)//
|
||||
.transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION.getValue())//
|
||||
.loanScheduleType("PROGRESSIVE") //
|
||||
.loanScheduleProcessingType("HORIZONTAL")//
|
||||
.interestRateFrequencyType(3)//
|
||||
.maxInterestRatePerPeriod(10.0)//
|
||||
.paymentAllocation(List.of(//
|
||||
createPaymentAllocation("DEFAULT", "NEXT_INSTALLMENT"), //
|
||||
createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"), //
|
||||
createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"), //
|
||||
createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT")));//
|
||||
final Response<PostLoanProductsResponse> responseLoanProductsRequestPrgAdvZeroIntChargeOffDelinquentReasonIntRecalcCapitalizedIncome = loanProductsApi
|
||||
.createLoanProduct(loanProductsRequestPrgAdvZeroIntChargeOffDelinquentReasonIntRecalcCapitalizedIncome).execute();
|
||||
TestContext.INSTANCE.set(
|
||||
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_ZERO_INT_CHARGE_OFF_DELINQUENT_REASON_INT_RECALC_CAPITALIZED_INCOME,
|
||||
responseLoanProductsRequestPrgAdvZeroIntChargeOffDelinquentReasonIntRecalcCapitalizedIncome);
|
||||
}
|
||||
|
||||
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
|
||||
|
||||
+77
@@ -26,6 +26,7 @@ import java.io.IOException;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
|
||||
@@ -138,6 +139,82 @@ public class JournalEntriesStepDef extends AbstractStepDef {
|
||||
}
|
||||
}
|
||||
|
||||
@Then("Reversed loan capitalized income amortization transaction has the following Journal entries:")
|
||||
public void capitalizedIncomeAmortizationJournalEntryDataCheck(final DataTable table) {
|
||||
final long capitalizedIncomeAmortizationId = testContext().get(TestContextKey.LOAN_CAPITALIZED_INCOME_AMORTIZATION_ID);
|
||||
final Response<PostLoansResponse> loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
|
||||
assert loanResponse.body() != null;
|
||||
final long loanId = loanResponse.body().getLoanId();
|
||||
final String resourceId = String.valueOf(loanId);
|
||||
|
||||
List<JournalEntryTransactionItem> journalLinesActualList;
|
||||
final String transactionId = "L" + capitalizedIncomeAmortizationId;
|
||||
Response<GetJournalEntriesTransactionIdResponse> journalEntryDataResponse = null;
|
||||
try {
|
||||
journalEntryDataResponse = journalEntriesApi.retrieveAll1(//
|
||||
null, //
|
||||
null, //
|
||||
null, //
|
||||
null, //
|
||||
null, //
|
||||
null, //
|
||||
null, //
|
||||
transactionId, //
|
||||
null, //
|
||||
null, //
|
||||
null, //
|
||||
null, //
|
||||
null, //
|
||||
null, //
|
||||
null, //
|
||||
loanId, //
|
||||
null, //
|
||||
null, //
|
||||
true//
|
||||
).execute();
|
||||
ErrorHelper.checkSuccessfulApiCall(journalEntryDataResponse);
|
||||
} catch (IOException e) {
|
||||
log.error("Exception", e);
|
||||
}
|
||||
assert journalEntryDataResponse != null;
|
||||
assert journalEntryDataResponse.body() != null;
|
||||
journalLinesActualList = journalEntryDataResponse.body().getPageItems();
|
||||
|
||||
final List<List<String>> data = table.asLists();
|
||||
for (int i = 1; i < data.size(); i++) {
|
||||
final List<List<String>> possibleActualValuesList = new ArrayList<>();
|
||||
final List<String> expectedValues = data.get(i);
|
||||
boolean containsAnyExpected = false;
|
||||
|
||||
for (int j = 0; j < Objects.requireNonNull(journalLinesActualList).size(); j++) {
|
||||
final JournalEntryTransactionItem journalLinesActual = journalLinesActualList.get(j);
|
||||
final List<String> actualValues = new ArrayList<>();
|
||||
assert journalLinesActual.getGlAccountType() != null;
|
||||
actualValues.add(
|
||||
journalLinesActual.getGlAccountType().getValue() == null ? null : journalLinesActual.getGlAccountType().getValue());
|
||||
actualValues.add(journalLinesActual.getGlAccountCode() == null ? null : journalLinesActual.getGlAccountCode());
|
||||
actualValues.add(journalLinesActual.getGlAccountName() == null ? null : journalLinesActual.getGlAccountName());
|
||||
assert journalLinesActual.getEntryType() != null;
|
||||
actualValues
|
||||
.add("DEBIT".equals(journalLinesActual.getEntryType().getValue()) ? String.valueOf(journalLinesActual.getAmount())
|
||||
: null);
|
||||
actualValues
|
||||
.add("CREDIT".equals(journalLinesActual.getEntryType().getValue()) ? String.valueOf(journalLinesActual.getAmount())
|
||||
: null);
|
||||
possibleActualValuesList.add(actualValues);
|
||||
|
||||
final boolean containsExpectedValues = possibleActualValuesList.stream()
|
||||
.anyMatch(actualValue -> actualValue.equals(expectedValues));
|
||||
if (containsExpectedValues) {
|
||||
containsAnyExpected = true;
|
||||
}
|
||||
}
|
||||
assertThat(containsAnyExpected)
|
||||
.as(ErrorMessageHelper.wrongValueInLineInJournalEntry(resourceId, i, possibleActualValuesList, expectedValues))
|
||||
.isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Then("In Loan transactions the replayed {string} transaction with date {string} has a reverted transaction pair with the following Journal entries:")
|
||||
public void revertedJournalEntryDataCheck(String transactionType, String transactionDate, DataTable table) throws IOException {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
|
||||
|
||||
+11
@@ -1692,6 +1692,17 @@ public class LoanStepDef extends AbstractStepDef {
|
||||
ErrorHelper.checkSuccessfulApiCall(chargeOffResponse);
|
||||
|
||||
Long transactionId = chargeOffResponse.body().getResourceId();
|
||||
|
||||
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
|
||||
final Response<GetLoansLoanIdResponse> loanDetailsResponse = loansApi.retrieveLoan(loanId, false, "transactions", "", "").execute();
|
||||
assert loanDetailsResponse.body() != null;
|
||||
final List<GetLoansLoanIdTransactions> transactions = loanDetailsResponse.body().getTransactions();
|
||||
final Optional<GetLoansLoanIdTransactions> transactionsMatch = transactions.stream()
|
||||
.filter(t -> formatter.format(t.getDate()).equals(transactionDate) && t.getType().getCapitalizedIncomeAmortization())
|
||||
.findFirst();
|
||||
if (transactionsMatch.isPresent()) {
|
||||
testContext().set(TestContextKey.LOAN_CAPITALIZED_INCOME_AMORTIZATION_ID, transactionsMatch.get().getId());
|
||||
}
|
||||
eventAssertion.assertEvent(LoanChargeOffEvent.class, transactionId).extractingData(LoanTransactionDataV1::getLoanId)
|
||||
.isEqualTo(loanId).extractingData(LoanTransactionDataV1::getId).isEqualTo(chargeOffResponse.body().getResourceId());
|
||||
}
|
||||
|
||||
+2
@@ -149,6 +149,7 @@ public abstract class TestContextKey {
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME = "loanProductCreateResponseLP2ProgressiveAdvPayment36030InterestRecalcCapitalizedIncome";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME_FEE = "loanProductCreateResponseLP2ProgressiveAdvPayment36030InterestRecalcCapitalizedIncomeFee";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSAL_CAPITALIZED_INCOME = "loanProductCreateResponseLP2ProgressiveAdvPayment36030InterestRecalcMultidisbursalCapitalizedIncome";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_ZERO_INT_CHARGE_OFF_DELINQUENT_REASON_INT_RECALC_CAPITALIZED_INCOME = "loanProductCreateResponseLP2AdvancedPaymentZeroInterestChargeOffDelinquentReasonInterestRecalculationCapitalizedIncome";
|
||||
public static final String LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_INTEREST_FIRST_RESPONSE = "loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationInterestFirst";
|
||||
public static final String LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_PRINCIPAL_FIRST_RESPONSE = "loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationPrincipalFirst";
|
||||
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_INTEREST_RECALCULATION_ACCELERATE_MATURITY_CHARGE_OFF_BEHAVIOUR_LAST_INSTALLMENT_STRATEGY = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyInterestRecalculationAccelerateMaturityChargeOffBehaviourLastInstallmentStrategy";
|
||||
@@ -209,4 +210,5 @@ public abstract class TestContextKey {
|
||||
public static final String LOAN_TRANSACTION_RESPONSE = "loanTransactionResponse";
|
||||
public static final String LOAN_CAPITALIZED_INCOME_RESPONSE = "loanCapitalizedIncomeResponse";
|
||||
public static final String LOAN_DISBURSEMENT_DETAIL_RESPONSE = "loanDisbursementDetailResponse";
|
||||
public static final String LOAN_CAPITALIZED_INCOME_AMORTIZATION_ID = "loanCapitalizedIncomeAmortizationId";
|
||||
}
|
||||
|
||||
@@ -269,6 +269,7 @@ Feature: Capitalized Income
|
||||
| EXPENSE | e4 | Written off | | 100.0 |
|
||||
| LIABILITY | 145024 | Deferred Capitalized Income | 100.0 | |
|
||||
Then LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent is raised on "02 January 2024"
|
||||
|
||||
@TestRailId:C3648
|
||||
Scenario: Verify capitalized income: daily amortization - Capitalized Income type: interest
|
||||
When Admin sets the business date to "01 January 2024"
|
||||
@@ -528,3 +529,194 @@ Feature: Capitalized Income
|
||||
| INCOME | 404000 | Interest Income | | 0.55 |
|
||||
| LIABILITY | 145024 | Deferred Capitalized Income | 0.55 | |
|
||||
And Loan Capitalized Income Amortization Transaction Created Business Event is created on "31 March 2024"
|
||||
|
||||
@TestRailId:C3661
|
||||
Scenario: As a user I want to add capitalized income to a progressive loan after disbursement and then charge-off the loan with "delinquent" reason - amortization in case of loan charge-off 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_ZERO_INTEREST_CHARGE_OFF_DELINQUENT_REASON_INTEREST_RECALC_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 |
|
||||
Then Admin can successfully set Fraud flag to the loan
|
||||
When Admin sets the business date to "03 February 2024"
|
||||
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 | |
|
||||
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 | 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 |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 100.0 |
|
||||
| LIABILITY | 145024 | Deferred Capitalized Income | 100.0 | |
|
||||
Then LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent is raised on "03 February 2024"
|
||||
|
||||
@TestRailId:C3662
|
||||
Scenario: As a user I want to add capitalized income to a progressive loan after disbursement and then charge-off a fraud loan - amortization in case of loan charge-off 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_ZERO_INTEREST_CHARGE_OFF_DELINQUENT_REASON_INTEREST_RECALC_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 |
|
||||
Then Admin can successfully set Fraud flag to the loan
|
||||
When Admin sets the business date to "03 February 2024"
|
||||
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 | |
|
||||
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 | 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 |
|
||||
| EXPENSE | 744037 | Credit Loss/Bad Debt-Fraud | | 100.0 |
|
||||
| LIABILITY | 145024 | Deferred Capitalized Income | 100.0 | |
|
||||
Then LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent is raised on "03 February 2024"
|
||||
|
||||
@TestRailId:C3663
|
||||
Scenario: As a user I want to add capitalized income to a progressive loan after disbursement and then charge-off the loan - amortization in case of loan charge-off 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_ZERO_INTEREST_CHARGE_OFF_DELINQUENT_REASON_INTEREST_RECALC_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 |
|
||||
Then Admin can successfully set Fraud flag to the loan
|
||||
When Admin sets the business date to "03 February 2024"
|
||||
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 | |
|
||||
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 | 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 |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 100.0 |
|
||||
| LIABILITY | 145024 | Deferred Capitalized Income | 100.0 | |
|
||||
Then LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent is raised on "03 February 2024"
|
||||
|
||||
@TestRailId:C3664
|
||||
Scenario: As a user I want to add capitalized income to a progressive loan after disbursement and then undo the charge-off transaction with "delinquent" reason - amortization in case of loan charge-off event should also be reversed
|
||||
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_ZERO_INTEREST_CHARGE_OFF_DELINQUENT_REASON_INTEREST_RECALC_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 |
|
||||
Then Admin can successfully set Fraud flag to the loan
|
||||
When Admin sets the business date to "03 February 2024"
|
||||
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 | |
|
||||
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 | 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 |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | | 100.0 |
|
||||
| LIABILITY | 145024 | Deferred Capitalized Income | 100.0 | |
|
||||
Then LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent is raised on "03 February 2024"
|
||||
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 |
|
||||
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 |
|
||||
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 |
|
||||
| LIABILITY | 145024 | Deferred Capitalized Income | 100.0 | |
|
||||
| EXPENSE | 744007 | Credit Loss/Bad Debt | 100.0 | |
|
||||
| LIABILITY | 145024 | Deferred Capitalized Income | | 100.0 |
|
||||
|
||||
+2
-1
@@ -2167,7 +2167,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
public LoanTransaction getLastUserTransaction() {
|
||||
return getLoanTransactions().stream() //
|
||||
.filter(t -> t.isNotReversed() && !(t.isAccrual() || t.isAccrualAdjustment() || t.isIncomePosting())) //
|
||||
.filter(t -> t.isNotReversed()
|
||||
&& !(t.isAccrual() || t.isAccrualAdjustment() || t.isIncomePosting() || t.isCapitalizedIncomeAmortization())) //
|
||||
.reduce((first, second) -> second) //
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
+4
@@ -646,6 +646,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
return LoanTransactionType.CAPITALIZED_INCOME.equals(getTypeOf()) && isNotReversed();
|
||||
}
|
||||
|
||||
public boolean isCapitalizedIncomeAmortization() {
|
||||
return LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION.equals(getTypeOf()) && isNotReversed();
|
||||
}
|
||||
|
||||
public boolean isWaiver() {
|
||||
return isInterestWaiver() || isChargesWaiver();
|
||||
}
|
||||
|
||||
+5
@@ -19,9 +19,14 @@
|
||||
package org.apache.fineract.portfolio.loanaccount.service;
|
||||
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
public interface LoanCapitalizedIncomeAmortizationProcessingService {
|
||||
|
||||
void processCapitalizedIncomeAmortizationOnLoanClosure(@NonNull Loan loan);
|
||||
|
||||
void processCapitalizedIncomeAmortizationOnLoanChargeOff(@NonNull Loan loan);
|
||||
|
||||
void processCapitalizedIncomeAmortizationOnLoanUndoChargeOff(@NonNull LoanTransaction loanTransaction);
|
||||
}
|
||||
|
||||
+77
@@ -173,6 +173,16 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
|
||||
private void createJournalEntriesForCapitalizedIncomeAmortization(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
|
||||
final Office office) {
|
||||
final boolean isMarkedAsChargeOff = loanDTO.isMarkedAsChargeOff();
|
||||
if (isMarkedAsChargeOff) {
|
||||
createJournalEntriesForChargeOffLoanCapitalizedIncomeAmortization(loanDTO, loanTransactionDTO, office);
|
||||
} else {
|
||||
createJournalEntriesForLoanCapitalizedIncomeAmortization(loanDTO, loanTransactionDTO, office);
|
||||
}
|
||||
}
|
||||
|
||||
private void createJournalEntriesForLoanCapitalizedIncomeAmortization(final LoanDTO loanDTO,
|
||||
final LoanTransactionDTO loanTransactionDTO, final Office office) {
|
||||
// loan properties
|
||||
final Long loanProductId = loanDTO.getLoanProductId();
|
||||
final Long loanId = loanDTO.getLoanId();
|
||||
@@ -218,6 +228,73 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
||||
}
|
||||
}
|
||||
|
||||
private void createJournalEntriesForChargeOffLoanCapitalizedIncomeAmortization(final LoanDTO loanDTO,
|
||||
final LoanTransactionDTO loanTransactionDTO, final Office office) {
|
||||
// loan properties
|
||||
final Long loanProductId = loanDTO.getLoanProductId();
|
||||
final boolean isMarkedFraud = loanDTO.isMarkedAsFraud();
|
||||
final Long loanId = loanDTO.getLoanId();
|
||||
final String currencyCode = loanDTO.getCurrencyCode();
|
||||
|
||||
// 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();
|
||||
final Long chargeOffReasonCodeValue = loanDTO.getChargeOffReasonCodeValue();
|
||||
|
||||
final ProductToGLAccountMapping mapping = chargeOffReasonCodeValue != null
|
||||
? helper.getChargeOffMappingByCodeValue(loanProductId, PortfolioProductType.LOAN, chargeOffReasonCodeValue)
|
||||
: null;
|
||||
|
||||
if (mapping != null) {
|
||||
final GLAccount accountDebit = this.helper.getLinkedGLAccountForLoanProduct(loanProductId,
|
||||
AccrualAccountsForLoan.DEFERRED_INCOME_LIABILITY.getValue(), paymentTypeId);
|
||||
// handle interest payment
|
||||
if (MathUtil.isGreaterThanZero(interestAmount)) {
|
||||
glAccountBalanceHolder.addToCredit(mapping.getGlAccount(), interestAmount);
|
||||
glAccountBalanceHolder.addToDebit(accountDebit, interestAmount);
|
||||
}
|
||||
// handle fees payment
|
||||
if (MathUtil.isGreaterThanZero(feesAmount)) {
|
||||
glAccountBalanceHolder.addToCredit(mapping.getGlAccount(), feesAmount);
|
||||
glAccountBalanceHolder.addToDebit(accountDebit, feesAmount);
|
||||
}
|
||||
} else {
|
||||
final AccrualAccountsForLoan creditAccountType = isMarkedFraud ? AccrualAccountsForLoan.CHARGE_OFF_FRAUD_EXPENSE
|
||||
: AccrualAccountsForLoan.CHARGE_OFF_EXPENSE;
|
||||
// handle interest payment
|
||||
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();
|
||||
|
||||
+34
@@ -24,9 +24,12 @@ 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.domain.loan.transaction.LoanChargeOffPostBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanUndoChargeOffBusinessEvent;
|
||||
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;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@@ -39,6 +42,9 @@ public class LoanCapitalizedIncomeAmortizationEventService {
|
||||
public void addListeners() {
|
||||
businessEventNotifierService.addPreBusinessEventListener(LoanCloseBusinessEvent.class, new LoanCloseListener());
|
||||
businessEventNotifierService.addPostBusinessEventListener(LoanBalanceChangedBusinessEvent.class, new LoanBalanceChangedListener());
|
||||
businessEventNotifierService.addPostBusinessEventListener(LoanChargeOffPostBusinessEvent.class, new LoanChargeOffEventListener());
|
||||
businessEventNotifierService.addPostBusinessEventListener(LoanUndoChargeOffBusinessEvent.class,
|
||||
new LoanUndoChargeOffEventListener());
|
||||
}
|
||||
|
||||
private final class LoanCloseListener implements BusinessEventListener<LoanCloseBusinessEvent> {
|
||||
@@ -68,4 +74,32 @@ public class LoanCapitalizedIncomeAmortizationEventService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class LoanChargeOffEventListener implements BusinessEventListener<LoanChargeOffPostBusinessEvent> {
|
||||
|
||||
@Override
|
||||
public void onBusinessEvent(final LoanChargeOffPostBusinessEvent event) {
|
||||
final LoanTransaction loanTransaction = event.get();
|
||||
final Loan loan = loanTransaction.getLoan();
|
||||
if (loan.getLoanProductRelatedDetail().isEnableIncomeCapitalization() && loan.isChargedOff() && loanTransaction.isChargeOff()) {
|
||||
log.debug("Loan charge-off on capitalized income amortization for loan {}", loan.getId());
|
||||
loanCapitalizedIncomeAmortizationProcessingService.processCapitalizedIncomeAmortizationOnLoanChargeOff(loan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class LoanUndoChargeOffEventListener implements BusinessEventListener<LoanUndoChargeOffBusinessEvent> {
|
||||
|
||||
@Override
|
||||
public void onBusinessEvent(final LoanUndoChargeOffBusinessEvent event) {
|
||||
final LoanTransaction loanTransaction = event.get();
|
||||
final Loan loan = loanTransaction.getLoan();
|
||||
final LoanStatus status = loan.getStatus();
|
||||
if (loan.getLoanProductRelatedDetail().isEnableIncomeCapitalization() && loanTransaction.getTypeOf().isChargeOff()
|
||||
&& !(loan.isChargedOff() || status.isClosedObligationsMet() || status.isClosedWrittenOff() || status.isOverpaid())) {
|
||||
log.debug("Loan undo charge-off on capitalized income amortization for loan {}", loan.getId());
|
||||
loanCapitalizedIncomeAmortizationProcessingService.processCapitalizedIncomeAmortizationOnLoanUndoChargeOff(loanTransaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+70
-14
@@ -20,9 +20,12 @@ 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.configuration.domain.ConfigurationDomainService;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
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;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
@@ -42,31 +45,84 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanJournalEntryPoster journalEntryPoster;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void processCapitalizedIncomeAmortizationOnLoanClosure(@NotNull final Loan loan) {
|
||||
final LocalDate finalAmortizationTransactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
|
||||
ExternalId externalId = ExternalId.empty();
|
||||
BigDecimal totalUnrecognizedAmountCapitalizedIncomeAmount = BigDecimal.ZERO;
|
||||
final LocalDate transactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
|
||||
final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, false);
|
||||
businessEventNotifierService
|
||||
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction));
|
||||
}
|
||||
|
||||
final boolean isExternalIdAutoGenerationEnabled = configurationDomainService.isExternalIdAutoGenerationEnabled();
|
||||
if (isExternalIdAutoGenerationEnabled) {
|
||||
@Override
|
||||
@Transactional
|
||||
public void processCapitalizedIncomeAmortizationOnLoanChargeOff(@NotNull final Loan loan) {
|
||||
final List<Long> existingTransactionIds = loan.findExistingTransactionIds();
|
||||
final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds();
|
||||
|
||||
LocalDate transactionDate = loan.getChargedOffOnDate();
|
||||
if (transactionDate == null) {
|
||||
transactionDate = DateUtils.getBusinessLocalDate();
|
||||
}
|
||||
|
||||
final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, true);
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
businessEventNotifierService
|
||||
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction));
|
||||
}
|
||||
|
||||
private LoanTransaction createCapitalizedIncomeAmortizationTransaction(final Loan loan, final LocalDate transactionDate,
|
||||
final boolean isChargeOff) {
|
||||
ExternalId externalId = ExternalId.empty();
|
||||
BigDecimal totalUnrecognizedAmount = BigDecimal.ZERO;
|
||||
|
||||
if (configurationDomainService.isExternalIdAutoGenerationEnabled()) {
|
||||
externalId = ExternalId.generate();
|
||||
}
|
||||
|
||||
for (LoanCapitalizedIncomeBalance balance : loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId())) {
|
||||
totalUnrecognizedAmountCapitalizedIncomeAmount = totalUnrecognizedAmountCapitalizedIncomeAmount
|
||||
.add(balance.getUnrecognizedAmount());
|
||||
final List<LoanCapitalizedIncomeBalance> balances = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId());
|
||||
for (LoanCapitalizedIncomeBalance balance : balances) {
|
||||
final BigDecimal unrecognizedAmount = balance.getUnrecognizedAmount();
|
||||
totalUnrecognizedAmount = totalUnrecognizedAmount.add(unrecognizedAmount);
|
||||
if (isChargeOff) {
|
||||
balance.setChargedOffAmount(unrecognizedAmount);
|
||||
}
|
||||
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));
|
||||
final LoanTransaction amortizationTransaction = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(),
|
||||
transactionDate, totalUnrecognizedAmount, externalId);
|
||||
|
||||
loan.addLoanTransaction(amortizationTransaction);
|
||||
loanTransactionRepository.saveAndFlush(amortizationTransaction);
|
||||
|
||||
return amortizationTransaction;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void processCapitalizedIncomeAmortizationOnLoanUndoChargeOff(@NotNull final LoanTransaction loanTransaction) {
|
||||
final Loan loan = loanTransaction.getLoan();
|
||||
final List<Long> existingTransactionIds = loan.findExistingTransactionIds();
|
||||
final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds();
|
||||
|
||||
loan.getLoanTransactions().stream().filter(LoanTransaction::isCapitalizedIncomeAmortization)
|
||||
.filter(transaction -> transaction.getTransactionDate().equals(loanTransaction.getTransactionDate()))
|
||||
.forEach(transaction -> {
|
||||
transaction.reverse();
|
||||
final LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(transaction);
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data));
|
||||
|
||||
});
|
||||
|
||||
for (LoanCapitalizedIncomeBalance balance : loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId())) {
|
||||
balance.setUnrecognizedAmount(balance.getChargedOffAmount());
|
||||
balance.setChargedOffAmount(BigDecimal.ZERO);
|
||||
}
|
||||
|
||||
journalEntryPoster.postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds);
|
||||
}
|
||||
|
||||
private LocalDate getFinalCapitalizedIncomeAmortizationTransactionDate(final Loan loan) {
|
||||
|
||||
Reference in New Issue
Block a user