FINERACT-2232: Validations and cob step fix

- Add validation for capitalized income transaction not being in the future
- CapitalizedIncomeAmortizationBusinessStep add filter against too early amortization
This commit is contained in:
adam.magyari
2025-06-02 13:22:02 +02:00
committed by Adam Saghy
parent 1aff3fc9b6
commit 5281c7b17d
4 changed files with 102 additions and 8 deletions
@@ -73,7 +73,7 @@ Feature: Capitalized Income
Then Loan status will be "ACTIVE"
When Admin sets the business date to "2 January 2024"
And Admin successfully disburse the loan on "2 January 2024" with "200" EUR transaction amount
When Admin sets the business date to "2 January 2024"
When Admin sets the business date to "3 January 2024"
And Admin adds capitalized income with "AUTOPAY" payment type to the loan on "3 January 2024" with "200" EUR transaction amount
Then Loan Repayment schedule has 6 periods, with the following data for periods:
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
@@ -36,6 +36,7 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
@@ -108,6 +109,12 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
"Transaction date cannot be before disbursement date");
}
// Validate transaction date is not in the future
if (transactionDate != null && transactionDate.isAfter(DateUtils.getBusinessLocalDate())) {
baseDataValidator.reset().parameter("transactionDate").failWithCode("cannot.be.in.the.future",
"Transaction date cannot be in the future");
}
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
@@ -142,7 +149,7 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, getCapitalizedIncomeAdjustmentParameters());
Validator.validateOrThrow("loan.capitalized.incomeAdjustment", baseDataValidator -> {
Validator.validateOrThrow("loan.capitalizedIncomeAdjustment", baseDataValidator -> {
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
validateLoanClientIsActive(loan);
validateLoanGroupIsActive(loan);
@@ -171,6 +178,12 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
"Transaction date cannot be before disbursement date");
}
// Validate transaction date is not in the future
if (transactionDate != null && transactionDate.isAfter(DateUtils.getBusinessLocalDate())) {
baseDataValidator.reset().parameter("transactionDate").failWithCode("cannot.be.in.the.future",
"Transaction date cannot be in the future");
}
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
@@ -66,12 +66,14 @@ public class CapitalizedIncomeAmortizationBusinessStep implements LoanCOBBusines
BigDecimal totalAmortizationAmount = BigDecimal.ZERO;
for (LoanCapitalizedIncomeBalance balance : balances) {
long daysUntilMaturity = businessDate.until(loan.getMaturityDate(), ChronoUnit.DAYS);
Money dailyAmortization = CapitalizedIncomeAmortizationUtil.calculateDailyAmortization(
loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), daysUntilMaturity, balance.getUnrecognizedAmount(),
loan.getCurrency().toData());
totalAmortizationAmount = totalAmortizationAmount.add(dailyAmortization.getAmount(), mc);
balance.setUnrecognizedAmount(balance.getUnrecognizedAmount().subtract(dailyAmortization.getAmount(), mc));
if (DateUtils.isAfterInclusive(businessDate, balance.getDate())) {
long daysUntilMaturity = businessDate.until(loan.getMaturityDate(), ChronoUnit.DAYS);
Money dailyAmortization = CapitalizedIncomeAmortizationUtil.calculateDailyAmortization(
loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), daysUntilMaturity,
balance.getUnrecognizedAmount(), loan.getCurrency().toData());
totalAmortizationAmount = totalAmortizationAmount.add(dailyAmortization.getAmount(), mc);
balance.setUnrecognizedAmount(balance.getUnrecognizedAmount().subtract(dailyAmortization.getAmount(), mc));
}
}
capitalizedIncomeBalanceRepository.saveAll(balances);
@@ -339,4 +339,83 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest {
);
});
}
@Test
public void testCapitalizedIncomeTransactionsNotInFuture() {
final AtomicReference<Long> loanIdRef = 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");
// Capitalized income cannot be in the future
Assertions.assertThrows(RuntimeException.class,
() -> loanTransactionHelper.addCapitalizedIncome(loanId, "1 February 2024", 100.0));
Long capitalizedIncomeId = loanTransactionHelper.addCapitalizedIncome(loanId, "1 January 2024", 100.0).getResourceId();
// Capitalized income adjustment cannot be in the future
Assertions.assertThrows(RuntimeException.class,
() -> loanTransactionHelper.capitalizedIncomeAdjustment(loanId, capitalizedIncomeId, "1 February 2024", 10.0));
});
}
@Test
public void testCapitalizedIncomeAmortizationShouldNotHappensForFutureBalances() {
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());
// random midday COB run
executeInlineCOB(loanId);
// verify no early amortization was created
verifyTransactions(loanId, //
transaction(100.0, "Disbursement", "01 January 2024"), //
transaction(100.0, "Capitalized Income", "01 January 2024") //
);
});
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") //
);
});
}
}