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, //
|
||||||
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_CAPITALIZED_INCOME_FEE, //
|
||||||
LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSAL_CAPITALIZED_INCOME, //
|
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
|
@Override
|
||||||
|
|||||||
+11
@@ -1628,4 +1628,15 @@ public class LoanProductsRequestFactory {
|
|||||||
.incomeFromCapitalizationAccountId(accountTypeResolver.resolve(DefaultAccountType.INTEREST_INCOME));//
|
.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);
|
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) {
|
public static String wrongDataInJournalEntriesGlAccountType(int line, String actual, String expected) {
|
||||||
return String.format("Wrong data in Journal entries, line %s / GL account type. " //
|
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);
|
+ "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(
|
TestContext.INSTANCE.set(
|
||||||
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME_FEE,
|
TestContextKey.DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_CAPITALIZED_INCOME_FEE,
|
||||||
responseLoanProductsRequestLP2ProgressiveAdvPayment36030InterestRecalcCapitalizedIncomeFee);
|
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,
|
public static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule,
|
||||||
|
|||||||
+77
@@ -26,6 +26,7 @@ import java.io.IOException;
|
|||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
|
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:")
|
@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 {
|
public void revertedJournalEntryDataCheck(String transactionType, String transactionDate, DataTable table) throws IOException {
|
||||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
|
||||||
|
|||||||
+11
@@ -1692,6 +1692,17 @@ public class LoanStepDef extends AbstractStepDef {
|
|||||||
ErrorHelper.checkSuccessfulApiCall(chargeOffResponse);
|
ErrorHelper.checkSuccessfulApiCall(chargeOffResponse);
|
||||||
|
|
||||||
Long transactionId = chargeOffResponse.body().getResourceId();
|
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)
|
eventAssertion.assertEvent(LoanChargeOffEvent.class, transactionId).extractingData(LoanTransactionDataV1::getLoanId)
|
||||||
.isEqualTo(loanId).extractingData(LoanTransactionDataV1::getId).isEqualTo(chargeOffResponse.body().getResourceId());
|
.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 = "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_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_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_INTEREST_FIRST_RESPONSE = "loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationInterestFirst";
|
||||||
public static final String LP2_NO_INTEREST_RECALCULATION_CHARGEBACK_ALLOCATION_PRINCIPAL_FIRST_RESPONSE = "loanProductCreateResponseLP2NoInterestRecalculationChargebackAllocationPrincipalFirst";
|
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";
|
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_TRANSACTION_RESPONSE = "loanTransactionResponse";
|
||||||
public static final String LOAN_CAPITALIZED_INCOME_RESPONSE = "loanCapitalizedIncomeResponse";
|
public static final String LOAN_CAPITALIZED_INCOME_RESPONSE = "loanCapitalizedIncomeResponse";
|
||||||
public static final String LOAN_DISBURSEMENT_DETAIL_RESPONSE = "loanDisbursementDetailResponse";
|
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 |
|
| EXPENSE | e4 | Written off | | 100.0 |
|
||||||
| LIABILITY | 145024 | Deferred Capitalized Income | 100.0 | |
|
| LIABILITY | 145024 | Deferred Capitalized Income | 100.0 | |
|
||||||
Then LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent is raised on "02 January 2024"
|
Then LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent is raised on "02 January 2024"
|
||||||
|
|
||||||
@TestRailId:C3648
|
@TestRailId:C3648
|
||||||
Scenario: Verify capitalized income: daily amortization - Capitalized Income type: interest
|
Scenario: Verify capitalized income: daily amortization - Capitalized Income type: interest
|
||||||
When Admin sets the business date to "01 January 2024"
|
When Admin sets the business date to "01 January 2024"
|
||||||
@@ -528,3 +529,194 @@ Feature: Capitalized Income
|
|||||||
| INCOME | 404000 | Interest Income | | 0.55 |
|
| INCOME | 404000 | Interest Income | | 0.55 |
|
||||||
| LIABILITY | 145024 | Deferred Capitalized 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"
|
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() {
|
public LoanTransaction getLastUserTransaction() {
|
||||||
return getLoanTransactions().stream() //
|
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) //
|
.reduce((first, second) -> second) //
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -646,6 +646,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
return LoanTransactionType.CAPITALIZED_INCOME.equals(getTypeOf()) && isNotReversed();
|
return LoanTransactionType.CAPITALIZED_INCOME.equals(getTypeOf()) && isNotReversed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isCapitalizedIncomeAmortization() {
|
||||||
|
return LoanTransactionType.CAPITALIZED_INCOME_AMORTIZATION.equals(getTypeOf()) && isNotReversed();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isWaiver() {
|
public boolean isWaiver() {
|
||||||
return isInterestWaiver() || isChargesWaiver();
|
return isInterestWaiver() || isChargesWaiver();
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -19,9 +19,14 @@
|
|||||||
package org.apache.fineract.portfolio.loanaccount.service;
|
package org.apache.fineract.portfolio.loanaccount.service;
|
||||||
|
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
|
|
||||||
public interface LoanCapitalizedIncomeAmortizationProcessingService {
|
public interface LoanCapitalizedIncomeAmortizationProcessingService {
|
||||||
|
|
||||||
void processCapitalizedIncomeAmortizationOnLoanClosure(@NonNull Loan loan);
|
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,
|
private void createJournalEntriesForCapitalizedIncomeAmortization(final LoanDTO loanDTO, final LoanTransactionDTO loanTransactionDTO,
|
||||||
final Office office) {
|
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
|
// loan properties
|
||||||
final Long loanProductId = loanDTO.getLoanProductId();
|
final Long loanProductId = loanDTO.getLoanProductId();
|
||||||
final Long loanId = loanDTO.getLoanId();
|
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,
|
private void createJournalEntriesForInterestPaymentWaiverOrInterestRefund(LoanDTO loanDTO, LoanTransactionDTO loanTransactionDTO,
|
||||||
Office office) {
|
Office office) {
|
||||||
final Long loanProductId = loanDTO.getLoanProductId();
|
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.BusinessEventListener;
|
||||||
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
|
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.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.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -39,6 +42,9 @@ public class LoanCapitalizedIncomeAmortizationEventService {
|
|||||||
public void addListeners() {
|
public void addListeners() {
|
||||||
businessEventNotifierService.addPreBusinessEventListener(LoanCloseBusinessEvent.class, new LoanCloseListener());
|
businessEventNotifierService.addPreBusinessEventListener(LoanCloseBusinessEvent.class, new LoanCloseListener());
|
||||||
businessEventNotifierService.addPostBusinessEventListener(LoanBalanceChangedBusinessEvent.class, new LoanBalanceChangedListener());
|
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> {
|
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.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
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.domain.loan.transaction.LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent;
|
||||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
@@ -42,31 +45,84 @@ public class LoanCapitalizedIncomeAmortizationProcessingServiceImpl implements L
|
|||||||
private final LoanTransactionRepository loanTransactionRepository;
|
private final LoanTransactionRepository loanTransactionRepository;
|
||||||
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
||||||
private final BusinessEventNotifierService businessEventNotifierService;
|
private final BusinessEventNotifierService businessEventNotifierService;
|
||||||
|
private final LoanJournalEntryPoster journalEntryPoster;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public void processCapitalizedIncomeAmortizationOnLoanClosure(@NotNull final Loan loan) {
|
public void processCapitalizedIncomeAmortizationOnLoanClosure(@NotNull final Loan loan) {
|
||||||
final LocalDate finalAmortizationTransactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
|
final LocalDate transactionDate = getFinalCapitalizedIncomeAmortizationTransactionDate(loan);
|
||||||
ExternalId externalId = ExternalId.empty();
|
final LoanTransaction amortizationTransaction = createCapitalizedIncomeAmortizationTransaction(loan, transactionDate, false);
|
||||||
BigDecimal totalUnrecognizedAmountCapitalizedIncomeAmount = BigDecimal.ZERO;
|
businessEventNotifierService
|
||||||
|
.notifyPostBusinessEvent(new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(amortizationTransaction));
|
||||||
|
}
|
||||||
|
|
||||||
final boolean isExternalIdAutoGenerationEnabled = configurationDomainService.isExternalIdAutoGenerationEnabled();
|
@Override
|
||||||
if (isExternalIdAutoGenerationEnabled) {
|
@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();
|
externalId = ExternalId.generate();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (LoanCapitalizedIncomeBalance balance : loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId())) {
|
final List<LoanCapitalizedIncomeBalance> balances = loanCapitalizedIncomeBalanceRepository.findAllByLoanId(loan.getId());
|
||||||
totalUnrecognizedAmountCapitalizedIncomeAmount = totalUnrecognizedAmountCapitalizedIncomeAmount
|
for (LoanCapitalizedIncomeBalance balance : balances) {
|
||||||
.add(balance.getUnrecognizedAmount());
|
final BigDecimal unrecognizedAmount = balance.getUnrecognizedAmount();
|
||||||
|
totalUnrecognizedAmount = totalUnrecognizedAmount.add(unrecognizedAmount);
|
||||||
|
if (isChargeOff) {
|
||||||
|
balance.setChargedOffAmount(unrecognizedAmount);
|
||||||
|
}
|
||||||
balance.setUnrecognizedAmount(BigDecimal.ZERO);
|
balance.setUnrecognizedAmount(BigDecimal.ZERO);
|
||||||
}
|
}
|
||||||
|
|
||||||
final LoanTransaction finalCapitalizedIncomeAmortization = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(),
|
final LoanTransaction amortizationTransaction = LoanTransaction.capitalizedIncomeAmortization(loan, loan.getOffice(),
|
||||||
finalAmortizationTransactionDate, totalUnrecognizedAmountCapitalizedIncomeAmount, externalId);
|
transactionDate, totalUnrecognizedAmount, externalId);
|
||||||
loan.addLoanTransaction(finalCapitalizedIncomeAmortization);
|
|
||||||
loanTransactionRepository.saveAndFlush(finalCapitalizedIncomeAmortization);
|
loan.addLoanTransaction(amortizationTransaction);
|
||||||
businessEventNotifierService.notifyPostBusinessEvent(
|
loanTransactionRepository.saveAndFlush(amortizationTransaction);
|
||||||
new LoanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent(finalCapitalizedIncomeAmortization));
|
|
||||||
|
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) {
|
private LocalDate getFinalCapitalizedIncomeAmortizationTransactionDate(final Loan loan) {
|
||||||
|
|||||||
Reference in New Issue
Block a user