Skip to content

Commit a58bdf4

Browse files
Jose Alberto Hernandezadamsaghy
authored andcommitted
FINERACT-2354: Validation of Re-age amount during submission
1 parent 4c22e2a commit a58bdf4

File tree

7 files changed

+162
-12
lines changed

7 files changed

+162
-12
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: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@
3636
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
3737
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
3838
import org.apache.fineract.infrastructure.core.domain.ExternalId;
39+
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
3940
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
4041
import org.apache.fineract.infrastructure.core.service.DateUtils;
4142
import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
43+
import org.apache.fineract.infrastructure.core.service.MathUtil;
4244
import org.apache.fineract.infrastructure.event.business.domain.loan.reaging.LoanReAgeBusinessEvent;
4345
import org.apache.fineract.infrastructure.event.business.domain.loan.reaging.LoanUndoReAgeBusinessEvent;
4446
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.reaging.LoanReAgeTransactionBusinessEvent;
@@ -55,7 +57,6 @@
5557
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
5658
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
5759
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
58-
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
5960
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
6061
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepaymentPeriodData;
6162
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
@@ -66,12 +67,10 @@
6667
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData;
6768
import org.apache.fineract.portfolio.loanaccount.repository.LoanCapitalizedIncomeBalanceRepository;
6869
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
69-
import org.apache.fineract.portfolio.loanaccount.service.InterestScheduleModelRepositoryWrapper;
7070
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
7171
import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
7272
import org.apache.fineract.portfolio.loanaccount.service.LoanRepaymentScheduleService;
7373
import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService;
74-
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionService;
7574
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
7675
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
7776
import org.apache.fineract.portfolio.note.domain.Note;
@@ -89,7 +88,6 @@ public class LoanReAgingService {
8988
private final ExternalIdFactory externalIdFactory;
9089
private final BusinessEventNotifierService businessEventNotifierService;
9190
private final LoanTransactionRepository loanTransactionRepository;
92-
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
9391
private final NoteRepository noteRepository;
9492
private final LoanChargeValidator loanChargeValidator;
9593
private final LoanUtilService loanUtilService;
@@ -99,8 +97,6 @@ public class LoanReAgingService {
9997
private final LoanRepaymentScheduleService loanRepaymentScheduleService;
10098
private final LoanReadPlatformService loanReadPlatformService;
10199
private final LoanCapitalizedIncomeBalanceRepository loanCapitalizedIncomeBalanceRepository;
102-
private final InterestScheduleModelRepositoryWrapper modelRepository;
103-
private final LoanTransactionService loanTransactionService;
104100

105101
public CommandProcessingResult reAge(final Long loanId, final JsonCommand command) {
106102
final Loan loan = loanAssembler.assembleFrom(loanId);
@@ -240,7 +236,15 @@ private LoanTransaction createReAgeTransaction(Loan loan, JsonCommand command) {
240236
}
241237
// in case of a reaging transaction, only the outstanding principal amount until the business date is considered
242238
Money txPrincipal = loan.getTotalPrincipalOutstandingUntil(transactionDate);
243-
BigDecimal txPrincipalAmount = txPrincipal.getAmount();
239+
final BigDecimal txPrincipalAmount = txPrincipal.getAmount();
240+
if (command.hasParameter(LoanReAgingApiConstants.transactionAmountParamName)) {
241+
final BigDecimal transactionAmount = command
242+
.bigDecimalValueOfParameterNamed(LoanReAgingApiConstants.transactionAmountParamName);
243+
if (!MathUtil.isEqualTo(txPrincipalAmount, transactionAmount)) {
244+
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.amount.not.match.with.calculated.reage.amount",
245+
"re-age amount is not matching with the calculated re-age amount", txPrincipalAmount);
246+
}
247+
}
244248

245249
final LoanTransaction reAgeTransaction = new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.REAGE, transactionDate,
246250
txPrincipalAmount, txPrincipalAmount, ZERO, ZERO, ZERO, null, false, null, txExternalId);
@@ -275,10 +279,10 @@ private LoanReAgeParameter createReAgeParameter(LoanTransaction reAgeTransaction
275279
}
276280

277281
private void persistNote(Loan loan, JsonCommand command, Map<String, Object> changes) {
278-
if (command.hasParameter("note")) {
279-
final String note = command.stringValueOfParameterNamed("note");
282+
if (command.hasParameter(LoanReAgingApiConstants.noteParamName)) {
283+
final String note = command.stringValueOfParameterNamed(LoanReAgingApiConstants.noteParamName);
280284
final Note newNote = Note.loanNote(loan, note);
281-
changes.put("note", note);
285+
changes.put(LoanReAgingApiConstants.noteParamName, note);
282286

283287
this.noteRepository.saveAndFlush(newNote);
284288
}

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

Lines changed: 32 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");
@@ -110,6 +136,10 @@ private void validateReAgeRequest(Loan loan, JsonCommand command) {
110136
}
111137
}
112138

139+
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed(LoanReAgingApiConstants.transactionAmountParamName);
140+
baseDataValidator.reset().parameter(LoanReAgingApiConstants.transactionAmountParamName).value(transactionAmount).ignoreIfNull()
141+
.positiveAmount();
142+
113143
throwExceptionIfValidationErrorsExist(dataValidationErrors);
114144
}
115145

@@ -193,7 +223,8 @@ private void validateReAgeOutstandingBalance(final Loan loan, final JsonCommand
193223
return;
194224
}
195225

196-
if (loan.getSummary().getTotalPrincipalOutstanding().compareTo(java.math.BigDecimal.ZERO) == 0) {
226+
final BigDecimal totalPrincipalOutstanding = loan.getSummary().getTotalPrincipalOutstanding();
227+
if (MathUtil.isZero(totalPrincipalOutstanding)) {
197228
throw new GeneralPlatformDomainRuleException("error.msg.loan.reage.no.outstanding.balance.to.reage",
198229
"Loan cannot be re-aged as there are no outstanding balances to be re-aged", loan.getId());
199230
}

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

Lines changed: 29 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,32 @@ 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+
PlatformApiDataValidationException result = assertThrows(PlatformApiDataValidationException.class,
312+
() -> underTest.validateReAge(loan, command));
313+
// then
314+
assertThat(result).isNotNull();
315+
assertThat(result.getGlobalisationMessageCode()).isEqualTo("validation.msg.validation.errors.exist");
316+
assertThat(result.getErrors().getFirst().getUserMessageGlobalisationCode())
317+
.isEqualTo("validation.msg.loan.reAge.transactionAmount.not.greater.than.zero");
318+
}
319+
291320
@Test
292321
public void testValidateReAge_ShouldThrowException_WhenStartDateIsBeforeMaturity() {
293322
// 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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -949,6 +949,81 @@ public void testReAgeTemplate_BiMonthlyLoan_CalculatedStartDateUsesOneMonthNotRe
949949
});
950950
}
951951

952+
@Test
953+
public void test_LoanReAgeTransactionWithTransactionAmount() {
954+
AtomicLong createdLoanId = new AtomicLong();
955+
956+
runAt("01 January 2023", () -> {
957+
// Create Client
958+
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
959+
960+
int numberOfRepayments = 3;
961+
int repaymentEvery = 1;
962+
963+
// Create Loan Product
964+
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() //
965+
.numberOfRepayments(numberOfRepayments) //
966+
.repaymentEvery(repaymentEvery) //
967+
.installmentAmountInMultiplesOf(null) //
968+
.enableDownPayment(true) //
969+
.disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)) //
970+
.enableAutoRepaymentForDownPayment(true) //
971+
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()); //
972+
973+
PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product);
974+
Long loanProductId = loanProductResponse.getResourceId();
975+
976+
// Apply and Approve Loan
977+
double amount = 1250.0;
978+
979+
PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)//
980+
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)//
981+
.repaymentEvery(repaymentEvery)//
982+
.loanTermFrequency(numberOfRepayments)//
983+
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
984+
.loanTermFrequencyType(RepaymentFrequencyType.MONTHS);
985+
986+
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest);
987+
988+
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
989+
approveLoanRequest(amount, "01 January 2023"));
990+
991+
Long loanId = approvedLoanResult.getLoanId();
992+
993+
// disburse Loan
994+
disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January 2023");
995+
createdLoanId.set(loanId);
996+
});
997+
998+
runAt("12 April 2023", () -> {
999+
long loanId = createdLoanId.get();
1000+
1001+
// try re-age transaction with transaction amount in Zero
1002+
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
1003+
() -> reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12 April 2023", 4,
1004+
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.name(), 0.0));
1005+
assertEquals(400, exception.getResponse().code());
1006+
assertTrue(exception.getMessage().contains("validation.msg.loan.reAge.transactionAmount.not.greater.than.zero"));
1007+
1008+
// try re-age transaction with transaction amount lower than outstanding
1009+
exception = assertThrows(CallFailedRuntimeException.class, () -> reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1,
1010+
"12 April 2023", 4, LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.name(), 900.0));
1011+
assertEquals(403, exception.getResponse().code());
1012+
assertTrue(exception.getMessage().contains("error.msg.loan.reage.amount.not.match.with.calculated.reage.amount"));
1013+
1014+
// try re-age transaction with transaction amount higher than outstanding
1015+
exception = assertThrows(CallFailedRuntimeException.class, () -> reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1,
1016+
"12 April 2023", 4, LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.name(), 5000.0));
1017+
assertEquals(403, exception.getResponse().code());
1018+
assertTrue(exception.getMessage().contains("error.msg.loan.reage.amount.not.match.with.calculated.reage.amount"));
1019+
1020+
reAgeLoan(loanId, RepaymentFrequencyType.MONTHS_STRING, 1, "12 April 2023", 4,
1021+
LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_FULL_INTEREST.name(), 937.5);
1022+
1023+
checkMaturityDates(loanId, LocalDate.of(2023, 7, 12), LocalDate.of(2023, 7, 12));
1024+
});
1025+
}
1026+
9521027
private HashMap<String, Object> getReAgeTemplate(Long loanId) {
9531028
final String GET_REAGE_TEMPLATE_URL = "/fineract-provider/api/v1/loans/" + loanId + "/transactions/template?command=reAge&"
9541029
+ Utils.TENANT_IDENTIFIER;

0 commit comments

Comments
 (0)