Skip to content

Commit 2bd9427

Browse files
author
Jose Alberto Hernandez
committed
FINERACT-2354: Validation of Re-age amount during submission
1 parent 2bde43e commit 2bd9427

File tree

7 files changed

+153
-11
lines changed

7 files changed

+153
-11
lines changed

fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ public JsonCommand(final Long resourceId, final JsonElement parsedCommand, final
178178
this.parsedCommand = parsedCommand;
179179
this.resourceId = resourceId;
180180
this.commandId = null;
181-
this.jsonCommand = null;
181+
this.jsonCommand = parsedCommand.toString();
182182
this.fromApiJsonHelper = fromApiJsonHelper;
183183
this.entityName = null;
184184
this.subresourceId = null;

fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanReAgingApiConstants.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,7 @@ public interface LoanReAgingApiConstants {
3131

3232
String reAgeInterestHandlingParamName = "reAgeInterestHandling";
3333
String reasonCodeValueIdParamName = "reasonCodeValueId";
34+
35+
String transactionAmountParamName = "transactionAmount";
36+
String noteParamName = "note";
3437
}

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingService.java

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
5656
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
5757
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
58-
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
5958
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
6059
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
6160
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
@@ -66,12 +65,10 @@
6665
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
6766
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
6867
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
69-
import org.apache.fineract.portfolio.loanaccount.service.InterestScheduleModelRepositoryWrapper;
7068
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
7169
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
7270
import org.apache.fineract.portfolio.loanaccount.service.LoanRepaymentScheduleService;
7371
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
74-
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
7572
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
7673
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
7774
import org.apache.fineract.portfolio.note.domain.Note;
@@ -89,7 +86,6 @@ public class LoanReAgingService {
8986
private final ExternalIdFactory externalIdFactory;
9087
private final BusinessEventNotifierService businessEventNotifierService;
9188
private final LoanTransactionRepository loanTransactionRepository;
92-
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
9389
private final NoteRepository noteRepository;
9490
private final LoanChargeValidator loanChargeValidator;
9591
private final LoanUtilService loanUtilService;
@@ -99,8 +95,6 @@ public class LoanReAgingService {
9995
private final LoanRepaymentScheduleService loanRepaymentScheduleService;
10096
private final LoanReadPlatformService loanReadPlatformService;
10197
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
102-
private final InterestScheduleModelRepositoryWrapper modelRepository;
103-
private final LoanTransactionService loanTransactionService;
10498

10599
public CommandProcessingResult reAge(final Long loanId, final JsonCommand command) {
106100
final Loan loan = loanAssembler.assembleFrom(loanId);
@@ -275,10 +269,10 @@ private LoanReAgeParameter createReAgeParameter(LoanTransaction reAgeTransaction
275269
}
276270

277271
private void persistNote(Loan loan, JsonCommand command, Map<String, Object> changes) {
278-
if (command.hasParameter("note")) {
279-
final String note = command.stringValueOfParameterNamed("note");
272+
if (command.hasParameter(LoanReAgingApiConstants.noteParamName)) {
273+
final String note = command.stringValueOfParameterNamed(LoanReAgingApiConstants.noteParamName);
280274
final Note newNote = Note.loanNote(loan, note);
281-
changes.put("note", note);
275+
changes.put(LoanReAgingApiConstants.noteParamName, note);
282276

283277
this.noteRepository.saveAndFlush(newNote);
284278
}

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidator.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,30 @@
2020

2121
import static org.apache.fineract.infrastructure.core.service.DateUtils.getBusinessLocalDate;
2222

23+
import com.google.gson.reflect.TypeToken;
24+
import java.lang.reflect.Type;
25+
import java.math.BigDecimal;
2326
import java.time.LocalDate;
2427
import java.util.ArrayList;
2528
import java.util.Comparator;
2629
import java.util.List;
2730
import java.util.Locale;
31+
import java.util.Map;
2832
import java.util.Optional;
2933
import lombok.RequiredArgsConstructor;
34+
import org.apache.commons.lang3.StringUtils;
3035
import org.apache.fineract.infrastructure.codes.domain.CodeValue;
3136
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepository;
3237
import org.apache.fineract.infrastructure.core.api.JsonCommand;
3338
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
3439
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
3540
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
41+
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
3642
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
43+
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
3744
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
3845
import org.apache.fineract.infrastructure.core.service.DateUtils;
46+
import org.apache.fineract.infrastructure.core.service.MathUtil;
3947
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
4048
import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
4149
import org.apache.fineract.portfolio.loanaccount.api.request.ReAgePreviewRequest;
@@ -54,8 +62,17 @@ public class LoanReAgingValidator {
5462

5563
private final LoanTransactionRepository loanTransactionRepository;
5664
private final CodeValueRepository codeValueRepository;
65+
private final FromJsonHelper fromApiJsonHelper;
66+
67+
private final List<String> reAgeSupportedParameters = List.of(LoanReAgingApiConstants.externalIdParameterName,
68+
LoanReAgingApiConstants.startDate, LoanReAgingApiConstants.frequencyType, LoanReAgingApiConstants.frequencyNumber,
69+
LoanReAgingApiConstants.numberOfInstallments, LoanReAgingApiConstants.reAgeInterestHandlingParamName,
70+
LoanReAgingApiConstants.reasonCodeValueIdParamName, LoanReAgingApiConstants.transactionAmountParamName,
71+
LoanReAgingApiConstants.localeParameterName, LoanReAgingApiConstants.dateFormatParameterName,
72+
LoanReAgingApiConstants.noteParamName);
5773

5874
public void validateReAge(Loan loan, JsonCommand command) {
75+
validateJSONAndCheckForUnsupportedParams(command.json());
5976
validateReAgeRequest(loan, command);
6077
validateReAgeBusinessRules(loan);
6178
validateReAgeOutstandingBalance(loan, command);
@@ -67,6 +84,15 @@ public void validateReAge(final Loan loan, final ReAgePreviewRequest reAgePrevie
6784
validateReAgeOutstandingBalance(loan, reAgePreviewRequest);
6885
}
6986

87+
private void validateJSONAndCheckForUnsupportedParams(final String json) {
88+
if (StringUtils.isBlank(json)) {
89+
throw new InvalidJsonException();
90+
}
91+
92+
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
93+
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, reAgeSupportedParameters);
94+
}
95+
7096
private void validateReAgeRequest(Loan loan, JsonCommand command) {
7197
List<ApiParameterError> dataValidationErrors = new ArrayList<>();
7298
DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.reAge");
@@ -193,10 +219,25 @@ private void validateReAgeOutstandingBalance(final Loan loan, final JsonCommand
193219
return;
194220
}
195221

196-
if (loan.getSummary().getTotalPrincipalOutstanding().compareTo(java.math.BigDecimal.ZERO) == 0) {
222+
final BigDecimal totalPrincipalOutstanding = loan.getSummary().getTotalPrincipalOutstanding();
223+
if (MathUtil.isZero(totalPrincipalOutstanding)) {
197224
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.no.outstanding.balance.to.reage",
198225
"Loan cannot be re-aged as there are no outstanding balances to be re-aged", loan.getId());
199226
}
227+
228+
if (command.parameterExists(LoanReAgingApiConstants.transactionAmountParamName)) {
229+
final BigDecimal transactionAmount = command
230+
.bigDecimalValueOfParameterNamed(LoanReAgingApiConstants.transactionAmountParamName);
231+
if (MathUtil.isZero(transactionAmount)) {
232+
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.transaction.amount.cannot.be.zero",
233+
"Loan re-age transaction amount can not be zero", loan.getId());
234+
}
235+
final BigDecimal totalReAgeAmount = totalPrincipalOutstanding.add(loan.getSummary().getTotalInterestOutstanding());
236+
if (transactionAmount.compareTo(totalReAgeAmount) > 0) {
237+
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.amount.greater.than.outstanding.balance",
238+
"re-age amount cannot be greater than the current outstanding amount", totalReAgeAmount);
239+
}
240+
}
200241
}
201242

202243
private void validateReAgeRequest(final Loan loan, final ReAgePreviewRequest reAgePreviewRequest) {

fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingValidatorTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ class LoanReAgingValidatorTest {
7070
@Mock
7171
private LoanTransactionRepository loanTransactionRepository;
7272

73+
@Mock
74+
private FromJsonHelper fromApiJsonHelper;
75+
7376
@InjectMocks
7477
private LoanReAgingValidator underTest;
7578

@@ -288,6 +291,30 @@ public void testValidateReAge_ShouldThrowException_WhenNumberOfInstallmentsIsNeg
288291
.isEqualTo("validation.msg.loan.reAge.numberOfInstallments.not.greater.than.zero");
289292
}
290293

294+
@Test
295+
public void testValidateReAge_ShouldThrowException_WhenTransactionAmountIsZero() {
296+
// given
297+
Loan loan = loan();
298+
JsonCommand command = makeJsonCommand("""
299+
{
300+
"externalId": "12345",
301+
"dateFormat": "%s",
302+
"locale": "en",
303+
"startDate": "%s",
304+
"frequencyType": "MONTHS",
305+
"frequencyNumber": 1,
306+
"numberOfInstallments": 1,
307+
"transactionAmount": 0
308+
}
309+
""".formatted(DATE_FORMAT, formatDate(afterMaturity)));
310+
// when
311+
GeneralPlatformDomainRuleException result = assertThrows(GeneralPlatformDomainRuleException.class,
312+
() -> underTest.validateReAge(loan, command));
313+
// then
314+
assertThat(result).isNotNull();
315+
assertThat(result.getGlobalisationMessageCode()).isEqualTo("error.msg.loan.reage.transaction.amount.cannot.be.zero");
316+
}
317+
291318
@Test
292319
public void testValidateReAge_ShouldThrowException_WhenStartDateIsBeforeMaturity() {
293320
// given

integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1016,6 +1016,11 @@ protected void executeInlineCOB(Long loanId) {
10161016

10171017
protected void reAgeLoan(Long loanId, String frequencyType, int frequencyNumber, String startDate, Integer numberOfInstallments,
10181018
String reAgeInterestHandling) {
1019+
reAgeLoan(loanId, frequencyType, frequencyNumber, startDate, numberOfInstallments, reAgeInterestHandling, null);
1020+
}
1021+
1022+
protected void reAgeLoan(Long loanId, String frequencyType, int frequencyNumber, String startDate, Integer numberOfInstallments,
1023+
String reAgeInterestHandling, Double transactionAmount) {
10191024
PostLoansLoanIdTransactionsRequest request = new PostLoansLoanIdTransactionsRequest();
10201025
request.setDateFormat(DATETIME_PATTERN);
10211026
request.setLocale("en");
@@ -1024,6 +1029,9 @@ protected void reAgeLoan(Long loanId, String frequencyType, int frequencyNumber,
10241029
request.setStartDate(startDate);
10251030
request.setNumberOfInstallments(numberOfInstallments);
10261031
request.setReAgeInterestHandling(reAgeInterestHandling);
1032+
if (transactionAmount != null) {
1033+
request.transactionAmount(transactionAmount);
1034+
}
10271035
loanTransactionHelper.reAge(loanId, request);
10281036
}
10291037

integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/reaging/LoanReAgingIntegrationTest.java

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -689,4 +689,73 @@ public void test_LoanReAgeTransactionWithInterestHandling() {
689689
});
690690
}
691691

692+
@Test
693+
public void test_LoanReAgeTransactionWithTransactionAmount() {
694+
AtomicLong createdLoanId = new AtomicLong();
695+
696+
runAt("01 January 2023", () -> {
697+
// Create Client
698+
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
699+
700+
int numberOfRepayments = 3;
701+
int repaymentEvery = 1;
702+
703+
// Create Loan Product
704+
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() //
705+
.numberOfRepayments(numberOfRepayments) //
706+
.repaymentEvery(repaymentEvery) //
707+
.installmentAmountInMultiplesOf(null) //
708+
.enableDownPayment(true) //
709+
.disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)) //
710+
.enableAutoRepaymentForDownPayment(true) //
711+
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); //
712+
713+
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
714+
Long loanProductId = loanProductResponse.getResourceId();
715+
716+
// Apply and Approve Loan
717+
double amount = 1250.0;
718+
719+
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
720+
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
721+
.repaymentEvery(repaymentEvery)//
722+
.loanTermFrequency(numberOfRepayments)//
723+
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
724+
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
725+
726+
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
727+
728+
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
729+
approveLoanRequest(amount, "01 January 2023"));
730+
731+
Long loanId = approvedLoanResult.getLoanId();
732+
733+
// disburse Loan
734+
disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
735+
createdLoanId.set(loanId);
736+
});
737+
738+
runAt("12 April 2023", () -> {
739+
long loanId = createdLoanId.get();
740+
741+
// try re-age transaction with transaction amount in Zero
742+
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
743+
() -> reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12 April 2023", 4,
744+
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.name(), 0.0));
745+
assertEquals(403, exception.getResponse().code());
746+
assertTrue(exception.getMessage().contains("error.msg.loan.reage.transaction.amount.cannot.be.zero"));
747+
748+
// try re-age transaction with transaction amount higher than outstanding
749+
exception = assertThrows(CallFailedRuntimeException.class, () -> reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1,
750+
"12 April 2023", 4, LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.name(), 5000.0));
751+
assertEquals(403, exception.getResponse().code());
752+
assertTrue(exception.getMessage().contains("error.msg.loan.reage.amount.greater.than.outstanding.balance"));
753+
754+
reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12 April 2023", 4,
755+
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.name(), 900.0);
756+
757+
checkMaturityDates(loanId, LocalDate.of(2023, 7, 12), LocalDate.of(2023, 7, 12));
758+
});
759+
}
760+
692761
}

0 commit comments

Comments
 (0)