FINERACT-2354: Validation of Re-age amount during submission
This commit is contained in:
committed by
Adam Saghy
parent
4c22e2a9fc
commit
a58bdf49b6
+1
-1
@@ -178,7 +178,7 @@ public final class JsonCommand {
|
||||
this.parsedCommand = parsedCommand;
|
||||
this.resourceId = resourceId;
|
||||
this.commandId = null;
|
||||
this.jsonCommand = null;
|
||||
this.jsonCommand = parsedCommand.toString();
|
||||
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||
this.entityName = null;
|
||||
this.subresourceId = null;
|
||||
|
||||
+3
@@ -31,4 +31,7 @@ public interface LoanReAgingApiConstants {
|
||||
|
||||
String reAgeInterestHandlingParamName = "reAgeInterestHandling";
|
||||
String reasonCodeValueIdParamName = "reasonCodeValueId";
|
||||
|
||||
String transactionAmountParamName = "transactionAmount";
|
||||
String noteParamName = "note";
|
||||
}
|
||||
|
||||
+14
-10
@@ -36,9 +36,11 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.reaging.LoanReAgeBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.reaging.LoanUndoReAgeBusinessEvent;
|
||||
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanReAgeTransactionBusinessEvent;
|
||||
@@ -55,7 +57,6 @@ import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLo
|
||||
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||
@@ -66,12 +67,10 @@ import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFou
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
|
||||
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
|
||||
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.InterestScheduleModelRepositoryWrapper;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanRepaymentScheduleService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
|
||||
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
|
||||
import org.apache.fineract.portfolio.note.domain.Note;
|
||||
@@ -89,7 +88,6 @@ public class LoanReAgingService {
|
||||
private final ExternalIdFactory externalIdFactory;
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
|
||||
private final NoteRepository noteRepository;
|
||||
private final LoanChargeValidator loanChargeValidator;
|
||||
private final LoanUtilService loanUtilService;
|
||||
@@ -99,8 +97,6 @@ public class LoanReAgingService {
|
||||
private final LoanRepaymentScheduleService loanRepaymentScheduleService;
|
||||
private final LoanReadPlatformService loanReadPlatformService;
|
||||
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
|
||||
private final InterestScheduleModelRepositoryWrapper modelRepository;
|
||||
private final LoanTransactionService loanTransactionService;
|
||||
|
||||
public CommandProcessingResult reAge(final Long loanId, final JsonCommand command) {
|
||||
final Loan loan = loanAssembler.assembleFrom(loanId);
|
||||
@@ -240,7 +236,15 @@ public class LoanReAgingService {
|
||||
}
|
||||
// in case of a reaging transaction, only the outstanding principal amount until the business date is considered
|
||||
Money txPrincipal = loan.getTotalPrincipalOutstandingUntil(transactionDate);
|
||||
BigDecimal txPrincipalAmount = txPrincipal.getAmount();
|
||||
final BigDecimal txPrincipalAmount = txPrincipal.getAmount();
|
||||
if (command.hasParameter(LoanReAgingApiConstants.transactionAmountParamName)) {
|
||||
final BigDecimal transactionAmount = command
|
||||
.bigDecimalValueOfParameterNamed(LoanReAgingApiConstants.transactionAmountParamName);
|
||||
if (!MathUtil.isEqualTo(txPrincipalAmount, transactionAmount)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.amount.not.match.with.calculated.reage.amount",
|
||||
"re-age amount is not matching with the calculated re-age amount", txPrincipalAmount);
|
||||
}
|
||||
}
|
||||
|
||||
final LoanTransaction reAgeTransaction = new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.REAGE, transactionDate,
|
||||
txPrincipalAmount, txPrincipalAmount, ZERO, ZERO, ZERO, null, false, null, txExternalId);
|
||||
@@ -275,10 +279,10 @@ public class LoanReAgingService {
|
||||
}
|
||||
|
||||
private void persistNote(Loan loan, JsonCommand command, Map<String, Object> changes) {
|
||||
if (command.hasParameter("note")) {
|
||||
final String note = command.stringValueOfParameterNamed("note");
|
||||
if (command.hasParameter(LoanReAgingApiConstants.noteParamName)) {
|
||||
final String note = command.stringValueOfParameterNamed(LoanReAgingApiConstants.noteParamName);
|
||||
final Note newNote = Note.loanNote(loan, note);
|
||||
changes.put("note", note);
|
||||
changes.put(LoanReAgingApiConstants.noteParamName, note);
|
||||
|
||||
this.noteRepository.saveAndFlush(newNote);
|
||||
}
|
||||
|
||||
+32
-1
@@ -20,22 +20,30 @@ package org.apache.fineract.portfolio.loanaccount.service.reaging;
|
||||
|
||||
import static org.apache.fineract.infrastructure.core.service.DateUtils.getBusinessLocalDate;
|
||||
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
|
||||
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||
import org.apache.fineract.infrastructure.core.service.MathUtil;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
|
||||
import org.apache.fineract.portfolio.loanaccount.api.request.ReAgePreviewRequest;
|
||||
@@ -54,8 +62,17 @@ public class LoanReAgingValidator {
|
||||
|
||||
private final LoanTransactionRepository loanTransactionRepository;
|
||||
private final CodeValueRepository codeValueRepository;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
private final List<String> reAgeSupportedParameters = List.of(LoanReAgingApiConstants.externalIdParameterName,
|
||||
LoanReAgingApiConstants.startDate, LoanReAgingApiConstants.frequencyType, LoanReAgingApiConstants.frequencyNumber,
|
||||
LoanReAgingApiConstants.numberOfInstallments, LoanReAgingApiConstants.reAgeInterestHandlingParamName,
|
||||
LoanReAgingApiConstants.reasonCodeValueIdParamName, LoanReAgingApiConstants.transactionAmountParamName,
|
||||
LoanReAgingApiConstants.localeParameterName, LoanReAgingApiConstants.dateFormatParameterName,
|
||||
LoanReAgingApiConstants.noteParamName);
|
||||
|
||||
public void validateReAge(Loan loan, JsonCommand command) {
|
||||
validateJSONAndCheckForUnsupportedParams(command.json());
|
||||
validateReAgeRequest(loan, command);
|
||||
validateReAgeBusinessRules(loan);
|
||||
validateReAgeOutstandingBalance(loan, command);
|
||||
@@ -67,6 +84,15 @@ public class LoanReAgingValidator {
|
||||
validateReAgeOutstandingBalance(loan, reAgePreviewRequest);
|
||||
}
|
||||
|
||||
private void validateJSONAndCheckForUnsupportedParams(final String json) {
|
||||
if (StringUtils.isBlank(json)) {
|
||||
throw new InvalidJsonException();
|
||||
}
|
||||
|
||||
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
|
||||
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, reAgeSupportedParameters);
|
||||
}
|
||||
|
||||
private void validateReAgeRequest(Loan loan, JsonCommand command) {
|
||||
List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.reAge");
|
||||
@@ -110,6 +136,10 @@ public class LoanReAgingValidator {
|
||||
}
|
||||
}
|
||||
|
||||
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(LoanReAgingApiConstants.transactionAmountParamName);
|
||||
baseDataValidator.reset().parameter(LoanReAgingApiConstants.transactionAmountParamName).value(transactionAmount).ignoreIfNull()
|
||||
.positiveAmount();
|
||||
|
||||
throwExceptionIfValidationErrorsExist(dataValidationErrors);
|
||||
}
|
||||
|
||||
@@ -193,7 +223,8 @@ public class LoanReAgingValidator {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loan.getSummary().getTotalPrincipalOutstanding().compareTo(java.math.BigDecimal.ZERO) == 0) {
|
||||
final BigDecimal totalPrincipalOutstanding = loan.getSummary().getTotalPrincipalOutstanding();
|
||||
if (MathUtil.isZero(totalPrincipalOutstanding)) {
|
||||
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.no.outstanding.balance.to.reage",
|
||||
"Loan cannot be re-aged as there are no outstanding balances to be re-aged", loan.getId());
|
||||
}
|
||||
|
||||
+29
@@ -70,6 +70,9 @@ class LoanReAgingValidatorTest {
|
||||
@Mock
|
||||
private LoanTransactionRepository loanTransactionRepository;
|
||||
|
||||
@Mock
|
||||
private FromJsonHelper fromApiJsonHelper;
|
||||
|
||||
@InjectMocks
|
||||
private LoanReAgingValidator underTest;
|
||||
|
||||
@@ -288,6 +291,32 @@ class LoanReAgingValidatorTest {
|
||||
.isEqualTo("validation.msg.loan.reAge.numberOfInstallments.not.greater.than.zero");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateReAge_ShouldThrowException_WhenTransactionAmountIsZero() {
|
||||
// given
|
||||
Loan loan = loan();
|
||||
JsonCommand command = makeJsonCommand("""
|
||||
{
|
||||
"externalId": "12345",
|
||||
"dateFormat": "%s",
|
||||
"locale": "en",
|
||||
"startDate": "%s",
|
||||
"frequencyType": "MONTHS",
|
||||
"frequencyNumber": 1,
|
||||
"numberOfInstallments": 1,
|
||||
"transactionAmount": 0
|
||||
}
|
||||
""".formatted(DATE_FORMAT, formatDate(afterMaturity)));
|
||||
// when
|
||||
PlatformApiDataValidationException result = assertThrows(PlatformApiDataValidationException.class,
|
||||
() -> underTest.validateReAge(loan, command));
|
||||
// then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
|
||||
assertThat(result.getErrors().getFirst().getUserMessageGlobalisationCode())
|
||||
.isEqualTo("validation.msg.loan.reAge.transactionAmount.not.greater.than.zero");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateReAge_ShouldThrowException_WhenStartDateIsBeforeMaturity() {
|
||||
// given
|
||||
|
||||
+8
@@ -1016,6 +1016,11 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
|
||||
protected void reAgeLoan(Long loanId, String frequencyType, int frequencyNumber, String startDate, Integer numberOfInstallments,
|
||||
String reAgeInterestHandling) {
|
||||
reAgeLoan(loanId, frequencyType, frequencyNumber, startDate, numberOfInstallments, reAgeInterestHandling, null);
|
||||
}
|
||||
|
||||
protected void reAgeLoan(Long loanId, String frequencyType, int frequencyNumber, String startDate, Integer numberOfInstallments,
|
||||
String reAgeInterestHandling, Double transactionAmount) {
|
||||
PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
|
||||
request.setDateFormat(DATETIME_PATTERN);
|
||||
request.setLocale("en");
|
||||
@@ -1024,6 +1029,9 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest {
|
||||
request.setStartDate(startDate);
|
||||
request.setNumberOfInstallments(numberOfInstallments);
|
||||
request.setReAgeInterestHandling(reAgeInterestHandling);
|
||||
if (transactionAmount != null) {
|
||||
request.transactionAmount(transactionAmount);
|
||||
}
|
||||
loanTransactionHelper.reAge(loanId, request);
|
||||
}
|
||||
|
||||
|
||||
+75
@@ -949,6 +949,81 @@ public class LoanReAgingIntegrationTest extends BaseLoanIntegrationTest {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_LoanReAgeTransactionWithTransactionAmount() {
|
||||
AtomicLong createdLoanId = new AtomicLong();
|
||||
|
||||
runAt("01 January 2023", () -> {
|
||||
// Create Client
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
|
||||
int numberOfRepayments = 3;
|
||||
int repaymentEvery = 1;
|
||||
|
||||
// Create Loan Product
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() //
|
||||
.numberOfRepayments(numberOfRepayments) //
|
||||
.repaymentEvery(repaymentEvery) //
|
||||
.installmentAmountInMultiplesOf(null) //
|
||||
.enableDownPayment(true) //
|
||||
.disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)) //
|
||||
.enableAutoRepaymentForDownPayment(true) //
|
||||
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); //
|
||||
|
||||
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
|
||||
Long loanProductId = loanProductResponse.getResourceId();
|
||||
|
||||
// Apply and Approve Loan
|
||||
double amount = 1250.0;
|
||||
|
||||
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
|
||||
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
|
||||
.repaymentEvery(repaymentEvery)//
|
||||
.loanTermFrequency(numberOfRepayments)//
|
||||
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
|
||||
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
|
||||
|
||||
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
|
||||
|
||||
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||
approveLoanRequest(amount, "01 January 2023"));
|
||||
|
||||
Long loanId = approvedLoanResult.getLoanId();
|
||||
|
||||
// disburse Loan
|
||||
disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
|
||||
createdLoanId.set(loanId);
|
||||
});
|
||||
|
||||
runAt("12 April 2023", () -> {
|
||||
long loanId = createdLoanId.get();
|
||||
|
||||
// try re-age transaction with transaction amount in Zero
|
||||
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||
() -> reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12 April 2023", 4,
|
||||
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.name(), 0.0));
|
||||
assertEquals(400, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("validation.msg.loan.reAge.transactionAmount.not.greater.than.zero"));
|
||||
|
||||
// try re-age transaction with transaction amount lower than outstanding
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1,
|
||||
"12 April 2023", 4, LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.name(), 900.0));
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.loan.reage.amount.not.match.with.calculated.reage.amount"));
|
||||
|
||||
// try re-age transaction with transaction amount higher than outstanding
|
||||
exception = assertThrows(CallFailedRuntimeException.class, () -> reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1,
|
||||
"12 April 2023", 4, LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.name(), 5000.0));
|
||||
assertEquals(403, exception.getResponse().code());
|
||||
assertTrue(exception.getMessage().contains("error.msg.loan.reage.amount.not.match.with.calculated.reage.amount"));
|
||||
|
||||
reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12 April 2023", 4,
|
||||
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.name(), 937.5);
|
||||
|
||||
checkMaturityDates(loanId, LocalDate.of(2023, 7, 12), LocalDate.of(2023, 7, 12));
|
||||
});
|
||||
}
|
||||
|
||||
private HashMap<String, Object> getReAgeTemplate(Long loanId) {
|
||||
final String GET_REAGE_TEMPLATE_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/template?command=reAge&"
|
||||
+ Utils.TENANT_IDENTIFIER;
|
||||
|
||||
Reference in New Issue
Block a user