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:
@@ -73,7 +73,7 @@ Feature: Capitalized Income
|
|||||||
Then Loan status will be "ACTIVE"
|
Then Loan status will be "ACTIVE"
|
||||||
When Admin sets the business date to "2 January 2024"
|
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
|
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
|
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:
|
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 |
|
| Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding |
|
||||||
|
|||||||
+14
-1
@@ -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.data.DataValidatorBuilder;
|
||||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
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.infrastructure.core.service.MathUtil;
|
||||||
import org.apache.fineract.organisation.holiday.domain.Holiday;
|
import org.apache.fineract.organisation.holiday.domain.Holiday;
|
||||||
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
|
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
|
||||||
@@ -108,6 +109,12 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
|
|||||||
"Transaction date cannot be before disbursement date");
|
"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);
|
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
|
||||||
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
|
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();
|
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, getCapitalizedIncomeAdjustmentParameters());
|
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);
|
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
|
||||||
validateLoanClientIsActive(loan);
|
validateLoanClientIsActive(loan);
|
||||||
validateLoanGroupIsActive(loan);
|
validateLoanGroupIsActive(loan);
|
||||||
@@ -171,6 +178,12 @@ public class ProgressiveLoanTransactionValidatorImpl implements ProgressiveLoanT
|
|||||||
"Transaction date cannot be before disbursement date");
|
"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);
|
final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
|
||||||
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
|
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
|
||||||
|
|
||||||
|
|||||||
+8
-6
@@ -66,12 +66,14 @@ public class CapitalizedIncomeAmortizationBusinessStep implements LoanCOBBusines
|
|||||||
|
|
||||||
BigDecimal totalAmortizationAmount = BigDecimal.ZERO;
|
BigDecimal totalAmortizationAmount = BigDecimal.ZERO;
|
||||||
for (LoanCapitalizedIncomeBalance balance : balances) {
|
for (LoanCapitalizedIncomeBalance balance : balances) {
|
||||||
long daysUntilMaturity = businessDate.until(loan.getMaturityDate(), ChronoUnit.DAYS);
|
if (DateUtils.isAfterInclusive(businessDate, balance.getDate())) {
|
||||||
Money dailyAmortization = CapitalizedIncomeAmortizationUtil.calculateDailyAmortization(
|
long daysUntilMaturity = businessDate.until(loan.getMaturityDate(), ChronoUnit.DAYS);
|
||||||
loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), daysUntilMaturity, balance.getUnrecognizedAmount(),
|
Money dailyAmortization = CapitalizedIncomeAmortizationUtil.calculateDailyAmortization(
|
||||||
loan.getCurrency().toData());
|
loan.getLoanProductRelatedDetail().getCapitalizedIncomeStrategy(), daysUntilMaturity,
|
||||||
totalAmortizationAmount = totalAmortizationAmount.add(dailyAmortization.getAmount(), mc);
|
balance.getUnrecognizedAmount(), loan.getCurrency().toData());
|
||||||
balance.setUnrecognizedAmount(balance.getUnrecognizedAmount().subtract(dailyAmortization.getAmount(), mc));
|
totalAmortizationAmount = totalAmortizationAmount.add(dailyAmortization.getAmount(), mc);
|
||||||
|
balance.setUnrecognizedAmount(balance.getUnrecognizedAmount().subtract(dailyAmortization.getAmount(), mc));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
capitalizedIncomeBalanceRepository.saveAll(balances);
|
capitalizedIncomeBalanceRepository.saveAll(balances);
|
||||||
|
|||||||
+79
@@ -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") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user