FINERACT-2232: Capitalized income - amortization in case of Charge-off event

This commit is contained in:
mariiaKraievska
2025-05-19 21:56:48 +02:00
committed by Arnold Gálovics
parent 39062ad1a1
commit 4861ad19fd
14 changed files with 523 additions and 15 deletions
@@ -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
@@ -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));//
}
}
@@ -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);
@@ -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,
@@ -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);
@@ -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());
}
@@ -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";
}