Skip to content

Commit 7a417b0

Browse files
committed
FINERACT-1971: Delinquency pause validation bug fixes
1 parent fb492b8 commit 7a417b0

3 files changed

Lines changed: 82 additions & 2 deletions

File tree

fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParseAndValidator.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public LoanDelinquencyAction validateAndParseUpdate(@NotNull final JsonCommand c
6161
} else if (DelinquencyAction.RESUME.equals(parsedDelinquencyAction.getAction())) {
6262
validateResumeStartDate(parsedDelinquencyAction, businessDate);
6363
validateResumeNoEndDate(parsedDelinquencyAction);
64+
validateResumeDoesNotExist(parsedDelinquencyAction, savedDelinquencyActions);
6465
validateResumeShouldBeOnActivePause(parsedDelinquencyAction, effectiveDelinquencyList);
6566
}
6667
return parsedDelinquencyAction;
@@ -87,6 +88,17 @@ private void validateResumeShouldBeOnActivePause(LoanDelinquencyAction parsedDel
8788
}
8889
}
8990

91+
private void validateResumeDoesNotExist(LoanDelinquencyAction parsedDelinquencyAction,
92+
List<LoanDelinquencyAction> savedDelinquencyActions) {
93+
boolean match = savedDelinquencyActions.stream() //
94+
.filter(action -> DelinquencyAction.RESUME.equals(action.getAction())) //
95+
.anyMatch(action -> parsedDelinquencyAction.getStartDate().isEqual(action.getStartDate()));
96+
if (match) {
97+
raiseValidationError("loan-delinquency-action-resume-should-be-unique",
98+
"There is an existing Resume Delinquency Action on this date");
99+
}
100+
}
101+
90102
private void validateResumeNoEndDate(LoanDelinquencyAction parsedDelinquencyAction) {
91103
if (parsedDelinquencyAction.getEndDate() != null) {
92104
raiseValidationError("loan-delinquency-action-resume-should-have-no-end-date",
@@ -163,7 +175,8 @@ private void validatePauseShallNotOverlap(LoanDelinquencyAction parsedDelinquenc
163175
*/
164176
private boolean isOverlapping(LoanDelinquencyAction parsed, LoanDelinquencyActionData existing) {
165177
return (parsed.getEndDate().isAfter(existing.getStartDate()) && parsed.getEndDate().isBefore(existing.getEndDate()))
166-
|| (parsed.getStartDate().isAfter(existing.getStartDate()) && parsed.getStartDate().isBefore(existing.getEndDate()));
178+
|| (parsed.getStartDate().isAfter(existing.getStartDate()) && parsed.getStartDate().isBefore(existing.getEndDate()))
179+
|| (parsed.getStartDate().isEqual(existing.getStartDate()) && parsed.getEndDate().isEqual(existing.getEndDate()));
167180
}
168181

169182
@org.jetbrains.annotations.NotNull

fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParseAndValidatorTest.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import java.util.List;
3737
import java.util.Locale;
3838
import java.util.Map;
39+
import java.util.Objects;
3940
import java.util.Optional;
4041
import org.apache.fineract.infrastructure.core.api.JsonCommand;
4142
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
@@ -136,6 +137,21 @@ public void testNewPauseEndIsOverlappingWithExistingPause() throws JsonProcessin
136137
() -> underTest.validateAndParseUpdate(command, loan, existing, localDate("09 September 2022")));
137138
}
138139

140+
@Test
141+
public void testNewPauseIsOverlappingWithExistingPauseBecauseSameDates() throws JsonProcessingException {
142+
Loan loan = Mockito.mock(Loan.class);
143+
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
144+
145+
List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "15 September 2022", "22 September 2022"));
146+
JsonCommand command = delinquencyAction("pause", "15 September 2022", "22 September 2022");
147+
List<LoanDelinquencyActionData> effectiveList = List.of(loanDelinquencyActionData(existing.get(0)));
148+
Mockito.when(delinquencyEffectivePauseHelper.calculateEffectiveDelinquencyList(existing)).thenReturn(effectiveList);
149+
150+
assertPlatformValidationException("Delinquency pause period cannot overlap with another pause period",
151+
"loan-delinquency-action-overlapping",
152+
() -> underTest.validateAndParseUpdate(command, loan, existing, localDate("09 September 2022")));
153+
}
154+
139155
@Test
140156
public void testNewPauseIsNotOverlappingBecauseThereWasAResume() throws JsonProcessingException {
141157
Loan loan = Mockito.mock(Loan.class);
@@ -214,6 +230,32 @@ public void testValidationErrorResumeInvalidStartDate() throws JsonProcessingExc
214230
() -> underTest.validateAndParseUpdate(command, loan, List.of(), localDate("10 September 2022")));
215231
}
216232

233+
@Test
234+
public void testValidationErrorResumeOnExistingResumeDate() throws JsonProcessingException {
235+
Loan loan = Mockito.mock(Loan.class);
236+
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
237+
238+
JsonCommand command = delinquencyAction("resume", "09 September 2022", null);
239+
List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "05 September 2022", "15 September 2022"));
240+
List<LoanDelinquencyActionData> effectiveList = List.of(loanDelinquencyActionData(existing.get(0)));
241+
Mockito.when(delinquencyEffectivePauseHelper.calculateEffectiveDelinquencyList(existing)).thenReturn(effectiveList);
242+
243+
LoanDelinquencyAction parsedDelinquencyAction = underTest.validateAndParseUpdate(command, loan, existing,
244+
localDate("09 September 2022"));
245+
Assertions.assertEquals(RESUME, parsedDelinquencyAction.getAction());
246+
Assertions.assertEquals(localDate("09 September 2022"), parsedDelinquencyAction.getStartDate());
247+
Assertions.assertNull(parsedDelinquencyAction.getEndDate());
248+
249+
List<LoanDelinquencyAction> existing2 = List.of(loanDelinquencyAction(PAUSE, "05 September 2022", "15 September 2022"),
250+
loanDelinquencyAction(RESUME, "09 September 2022", null));
251+
252+
JsonCommand command2 = delinquencyAction("resume", "09 September 2022", null);
253+
254+
assertPlatformValidationException("There is an existing Resume Delinquency Action on this date",
255+
"loan-delinquency-action-resume-should-be-unique",
256+
() -> underTest.validateAndParseUpdate(command2, loan, existing2, localDate("09 September 2022")));
257+
}
258+
217259
@Test
218260
public void testValidationErrorPausePeriodShouldBeAtLeastOneDay() throws JsonProcessingException {
219261
Loan loan = Mockito.mock(Loan.class);
@@ -348,7 +390,7 @@ private void assertPlatformException(String expectedMessage, String expectedCode
348390
}
349391

350392
private LoanDelinquencyAction loanDelinquencyAction(DelinquencyAction action, String startTime, String endTime) {
351-
return new LoanDelinquencyAction(null, action, localDate(startTime), localDate(endTime));
393+
return new LoanDelinquencyAction(null, action, localDate(startTime), Objects.isNull(endTime) ? null : localDate(endTime));
352394
}
353395

354396
private LoanDelinquencyActionData loanDelinquencyActionData(LoanDelinquencyAction loanDelinquencyAction) {

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,31 @@ public void testValidationErrorIsThrownWhenCreatingActionInThePast() {
222222
});
223223
}
224224

225+
@Test
226+
public void testValidationErrorIsThrownWhenCreatingActionThatOverlaps() {
227+
runAt("01 January 2023", () -> {
228+
// Create Client
229+
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
230+
231+
// Create Loan Product
232+
Long loanProductId = createLoanProductWith25PctDownPayment(true, true);
233+
234+
// Apply and Approve Loan
235+
Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2023", 1500.0, 2);
236+
237+
// Disburse Loan
238+
disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January 2023");
239+
240+
// Create Delinquency Pause for the Loan
241+
loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, "01 January 2023", "15 January 2023");
242+
243+
// Create overlapping Delinquency Pause for the Loan
244+
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
245+
() -> loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, "01 January 2023", "15 January 2023"));
246+
assertTrue(exception.getMessage().contains("Delinquency pause period cannot overlap with another pause period"));
247+
});
248+
}
249+
225250
private void validateLoanDelinquencyPausePeriods(Long loanId, GetLoansLoanIdDelinquencyPausePeriod... pausePeriods) {
226251
GetLoansLoanIdResponse loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
227252
Assertions.assertNotNull(loan.getDelinquent());

0 commit comments

Comments
 (0)