FINERACT-2232: Capitalized income adjustment

This commit is contained in:
adam.magyari
2025-05-29 16:19:29 +02:00
committed by Adam Saghy
parent 94cc68ba7c
commit 82eafad8dd
23 changed files with 672 additions and 10 deletions
@@ -19,12 +19,17 @@
package org.apache.fineract.integrationtests;
import java.math.BigDecimal;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdTransactions;
import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
import org.apache.fineract.integrationtests.common.BusinessStepHelper;
import org.apache.fineract.integrationtests.common.ClientHelper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@@ -96,4 +101,197 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
);
});
}
@Test
public void testLoanCapitalizedIncomeAdjustment() {
final AtomicReference<Long> loanIdRef = new AtomicReference<>();
final AtomicReference<Long> capitalizedIncomeIdRef = new AtomicReference<>();
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
.createLoanProduct(create4IProgressive().enableIncomeCapitalization(true)
.capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)
.capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)
.deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue())
.incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue())
.capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE));
runAt("1 April 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
500.0, 7.0, 3, null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
PostLoansLoanIdTransactionsResponse capitalizedIncomeResponse = loanTransactionHelper.addCapitalizedIncome(loanId,
"1 January 2024", 50.0);
capitalizedIncomeIdRef.set(capitalizedIncomeResponse.getResourceId());
loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "1 April 2024", 50.0);
verifyTransactions(loanId, //
transaction(100.0, "Disbursement", "01 January 2024"), //
transaction(50.0, "Capitalized Income", "01 January 2024"), //
transaction(50.0, "Capitalized Income Adjustment", "01 April 2024") //
);
verifyJournalEntries(loanId, //
journalEntry(100, loansReceivableAccount, "DEBIT"), //
journalEntry(100, fundSource, "CREDIT"), //
journalEntry(50, loansReceivableAccount, "DEBIT"), //
journalEntry(50, deferredIncomeLiabilityAccount, "CREDIT"), //
journalEntry(50.0, deferredIncomeLiabilityAccount, "DEBIT"), //
journalEntry(49.71, loansReceivableAccount, "CREDIT"), //
journalEntry(0.29, interestReceivableAccount, "CREDIT") //
);
});
}
@Test
public void testLoanCapitalizedIncomeAdjustmentValidations() {
final AtomicReference<Long> loanIdRef = new AtomicReference<>();
final AtomicReference<Long> capitalizedIncomeIdRef = new AtomicReference<>();
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
.createLoanProduct(create4IProgressive().enableIncomeCapitalization(true)
.capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)
.capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)
.deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue())
.incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue())
.capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE));
runAt("3 January 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
500.0, 7.0, 3, null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
PostLoansLoanIdTransactionsResponse capitalizedIncomeResponse = loanTransactionHelper.addCapitalizedIncome(loanId,
"3 January 2024", 50.0);
capitalizedIncomeIdRef.set(capitalizedIncomeResponse.getResourceId());
// Amount more than remaining
Assertions.assertThrows(RuntimeException.class,
() -> loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "3 January 2024", 60.0));
loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "3 January 2024", 30.0);
Assertions.assertThrows(RuntimeException.class,
() -> loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "3 January 2024", 30.0));
// Capitalized income transaction with given id doesn't exist for this loan
Assertions.assertThrows(RuntimeException.class,
() -> loanTransactionHelper.capitalizedIncomeAdjustment(loanId, 1L, "3 January 2024", 30.0));
// Cannot be earlier than capitalized income transaction
Assertions.assertThrows(RuntimeException.class,
() -> loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "2 January 2024", 30.0));
});
}
@Test
public void testLoanCapitalizedIncomeAdjustmentWithAmortizationAccounting() {
final AtomicReference<Long> loanIdRef = new AtomicReference<>();
final AtomicReference<Long> capitalizedIncomeIdRef = new AtomicReference<>();
final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
final PostLoanProductsResponse loanProductsResponse = loanProductHelper
.createLoanProduct(create4IProgressive().enableIncomeCapitalization(true)
.capitalizedIncomeCalculationType(PostLoanProductsRequest.CapitalizedIncomeCalculationTypeEnum.FLAT)
.capitalizedIncomeStrategy(PostLoanProductsRequest.CapitalizedIncomeStrategyEnum.EQUAL_AMORTIZATION)
.deferredIncomeLiabilityAccountId(deferredIncomeLiabilityAccount.getAccountID().longValue())
.incomeFromCapitalizationAccountId(feeIncomeAccount.getAccountID().longValue())
.capitalizedIncomeType(PostLoanProductsRequest.CapitalizedIncomeTypeEnum.FEE));
runAt("1 January 2024", () -> {
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "1 January 2024",
500.0, 7.0, 3, null);
loanIdRef.set(loanId);
disburseLoan(loanId, BigDecimal.valueOf(100), "1 January 2024");
PostLoansLoanIdTransactionsResponse capitalizedIncomeResponse = loanTransactionHelper.addCapitalizedIncome(loanId,
"1 January 2024", 100.0);
capitalizedIncomeIdRef.set(capitalizedIncomeResponse.getResourceId());
});
runAt("2 January 2024", () -> {
Long loanId = loanIdRef.get();
executeInlineCOB(loanId);
verifyTransactions(loanId, //
transaction(100.0, "Disbursement", "01 January 2024"), //
transaction(100.0, "Capitalized Income", "01 January 2024"), //
transaction(1.10, "Capitalized Income Amortization", "01 January 2024") //
);
});
runAt("3 January 2024", () -> {
Long loanId = loanIdRef.get();
executeInlineCOB(loanId);
verifyTransactions(loanId, //
transaction(100.0, "Disbursement", "01 January 2024"), //
transaction(100.0, "Capitalized Income", "01 January 2024"), //
transaction(1.10, "Capitalized Income Amortization", "01 January 2024"), //
transaction(0.04, "Accrual", "02 January 2024"), //
transaction(1.10, "Capitalized Income Amortization", "02 January 2024") //
);
verifyJournalEntries(loanId, //
journalEntry(100, loansReceivableAccount, "DEBIT"), //
journalEntry(100, fundSource, "CREDIT"), //
journalEntry(100, loansReceivableAccount, "DEBIT"), //
journalEntry(100, deferredIncomeLiabilityAccount, "CREDIT"), //
journalEntry(1.10, deferredIncomeLiabilityAccount, "DEBIT"), //
journalEntry(1.10, feeIncomeAccount, "CREDIT"), //
journalEntry(0.04, interestReceivableAccount, "DEBIT"), //
journalEntry(0.04, interestIncomeAccount, "CREDIT"), //
journalEntry(1.10, deferredIncomeLiabilityAccount, "DEBIT"), //
journalEntry(1.10, feeIncomeAccount, "CREDIT") //
);
loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeIdRef.get(), "3 January 2024", 100.0);
verifyTransactions(loanId, //
transaction(100.0, "Disbursement", "01 January 2024"), //
transaction(100.0, "Capitalized Income", "01 January 2024"), //
transaction(1.10, "Capitalized Income Amortization", "01 January 2024"), //
transaction(0.04, "Accrual", "02 January 2024"), //
transaction(1.10, "Capitalized Income Amortization", "02 January 2024"), //
transaction(100.0, "Capitalized Income Adjustment", "03 January 2024") //
);
verifyJournalEntries(loanId, //
journalEntry(100, loansReceivableAccount, "DEBIT"), //
journalEntry(100, fundSource, "CREDIT"), //
journalEntry(100, loansReceivableAccount, "DEBIT"), //
journalEntry(100, deferredIncomeLiabilityAccount, "CREDIT"), //
journalEntry(1.10, deferredIncomeLiabilityAccount, "DEBIT"), //
journalEntry(1.10, feeIncomeAccount, "CREDIT"), //
journalEntry(0.04, interestReceivableAccount, "DEBIT"), //
journalEntry(0.04, interestIncomeAccount, "CREDIT"), //
journalEntry(1.10, deferredIncomeLiabilityAccount, "DEBIT"), //
journalEntry(1.10, feeIncomeAccount, "CREDIT"), //
journalEntry(99.92, loansReceivableAccount, "CREDIT"), //
journalEntry(0.08, interestReceivableAccount, "CREDIT"), //
journalEntry(2.20, feeIncomeAccount, "DEBIT"), //
journalEntry(97.80, deferredIncomeLiabilityAccount, "DEBIT") //
);
// Reverse-replay
addRepaymentForLoan(loanId, 67.45, "2 January 2024");
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
Optional<GetLoansLoanIdTransactions> replayedCapitalizedIncomeAdjustmentOpt = loanDetails.getTransactions().stream()
.filter(t -> t.getType().getCapitalizedIncomeAdjustment()).findFirst();
Assertions.assertTrue(replayedCapitalizedIncomeAdjustmentOpt.isPresent(), "Capitalized income adjustment not found");
verifyTRJournalEntries(replayedCapitalizedIncomeAdjustmentOpt.get().getId(), //
journalEntry(99.98, loansReceivableAccount, "CREDIT"), //
journalEntry(0.02, interestReceivableAccount, "CREDIT"), //
journalEntry(2.20, feeIncomeAccount, "DEBIT"), //
journalEntry(97.80, deferredIncomeLiabilityAccount, "DEBIT") //
);
});
}
}
@@ -589,6 +589,12 @@ public class ExternalEventConfigurationHelper {
loanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent.put("enabled", false);
defaults.add(loanCapitalizedIncomeAmortizationTransactionCreatedBusinessEvent);
Map<String, Object> loanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent = new HashMap<>();
loanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent.put("type",
"LoanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent");
loanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent.put("enabled", false);
defaults.add(loanCapitalizedIncomeAdjustmentTransactionCreatedBusinessEvent);
return defaults;
}
@@ -933,6 +933,36 @@ public class LoanTransactionHelper {
new InterestPauseRequestDto().startDate(startDate).endDate(endDate).dateFormat(DATE_FORMAT).locale("en")));
}
public PostLoansLoanIdTransactionsResponse capitalizedIncomeAdjustment(final Long loanId, final Long capitalizedIncomeTransactionId,
final PostLoansLoanIdTransactionsTransactionIdRequest request) {
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.adjustLoanTransaction(loanId,
capitalizedIncomeTransactionId, request, "capitalizedIncomeAdjustment"));
}
public PostLoansLoanIdTransactionsResponse capitalizedIncomeAdjustment(final String loanExternalId, final Long transactionId,
final PostLoansLoanIdTransactionsTransactionIdRequest request) {
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.adjustLoanTransaction2(loanExternalId, transactionId,
request, "capitalizedIncomeAdjustment"));
}
public PostLoansLoanIdTransactionsResponse capitalizedIncomeAdjustment(final String loanExternalId, final String transactionExternalId,
final PostLoansLoanIdTransactionsTransactionIdRequest request) {
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.adjustLoanTransaction3(loanExternalId,
transactionExternalId, request, "capitalizedIncomeAdjustment"));
}
public PostLoansLoanIdTransactionsResponse capitalizedIncomeAdjustment(final Long loanId, final String transactionExternalId,
final PostLoansLoanIdTransactionsTransactionIdRequest request) {
return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.adjustLoanTransaction1(loanId, transactionExternalId,
request, "capitalizedIncomeAdjustment"));
}
public PostLoansLoanIdTransactionsResponse capitalizedIncomeAdjustment(final Long loanId, final Long capitalizedIncomeTransactionId,
final String transactionDate, final double amount) {
return capitalizedIncomeAdjustment(loanId, capitalizedIncomeTransactionId, new PostLoansLoanIdTransactionsTransactionIdRequest()
.transactionAmount(amount).transactionDate(transactionDate).dateFormat("dd MMMM yyyy").locale("en"));
}
// TODO: Rewrite to use fineract-client instead!
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest)