Skip to content

Commit

Permalink
FINERACT-1992: Backdated delinquency pause
Browse files Browse the repository at this point in the history
  • Loading branch information
ruchiD authored and adamsaghy committed Dec 13, 2023
1 parent da2cdd8 commit 6526e10
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiConstants;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMappings;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMappingsRepository;
Expand Down Expand Up @@ -230,8 +231,12 @@ public CommandProcessingResult createDelinquencyAction(Long loanId, JsonCommand
savedDelinquencyList, businessDate);

parsedDelinquencyAction.setLoan(loan);

LoanDelinquencyAction saved = loanDelinquencyActionRepository.saveAndFlush(parsedDelinquencyAction);
// if backdated pause recalculate delinquency data
if (DateUtils.isBefore(parsedDelinquencyAction.getStartDate(), businessDate)
&& DelinquencyAction.PAUSE.equals(parsedDelinquencyAction.getAction())) {
recalculateLoanDelinquencyData(loan);
}
businessEventNotifierService.notifyPostBusinessEvent(new LoanAccountDelinquencyPauseChangedBusinessEvent(loan));
return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
.withEntityId(saved.getId()) //
Expand All @@ -242,6 +247,21 @@ public CommandProcessingResult createDelinquencyAction(Long loanId, JsonCommand
.build();
}

private void recalculateLoanDelinquencyData(Loan loan) {
List<LoanDelinquencyAction> savedDelinquencyList = delinquencyReadPlatformService.retrieveLoanDelinquencyActions(loan.getId());
List<LoanDelinquencyActionData> effectiveDelinquencyList = delinquencyEffectivePauseHelper
.calculateEffectiveDelinquencyList(savedDelinquencyList);

CollectionData loanDelinquencyData = loanDelinquencyDomainService.getOverdueCollectionData(loan, effectiveDelinquencyList);
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(loan.getId(),
loanDelinquencyData.getDelinquentDate(), loanDelinquencyData.getDelinquentDays(), loan);
if (loanScheduleDelinquencyData.getOverdueDays() > 0) {
applyDelinquencyTagToLoan(loanScheduleDelinquencyData, effectiveDelinquencyList);
} else {
removeDelinquencyTagToLoan(loan);
}
}

@Override
public void removeDelinquencyTagToLoan(final Loan loan) {
if (loan.isEnableInstallmentLevelDelinquency()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ public LoanDelinquencyAction validateAndParseUpdate(@NotNull final JsonCommand c
validateLoanIsActive(loan);
if (DelinquencyAction.PAUSE.equals(parsedDelinquencyAction.getAction())) {
validateBothStartAndEndDatesAreProvided(parsedDelinquencyAction);
validatePauseStartAndEndDate(parsedDelinquencyAction, businessDate);
validatePauseStartAndEndDate(parsedDelinquencyAction);
validatePauseStartDateNotBeforeDisbursementDate(parsedDelinquencyAction, loan.getDisbursementDate());
validatePauseShallNotOverlap(parsedDelinquencyAction, effectiveDelinquencyList);
} else if (DelinquencyAction.RESUME.equals(parsedDelinquencyAction.getAction())) {
validateResumeStartDate(parsedDelinquencyAction, businessDate);
Expand Down Expand Up @@ -117,15 +118,18 @@ private void validateResumeStartDate(LoanDelinquencyAction parsedDelinquencyActi
}
}

private void validatePauseStartAndEndDate(LoanDelinquencyAction parsedDelinquencyAction, LocalDate businessDate) {
private void validatePauseStartAndEndDate(LoanDelinquencyAction parsedDelinquencyAction) {
if (parsedDelinquencyAction.getStartDate().equals(parsedDelinquencyAction.getEndDate())) {
raiseValidationError("loan-delinquency-action-invalid-start-date-and-end-date",
"Delinquency pause period must be at least one day");
}
}

if (businessDate.isAfter(parsedDelinquencyAction.getStartDate())) {
raiseValidationError("loan-delinquency-action-invalid-start-date", "Start date of pause period must be in the future",
START_DATE);
private void validatePauseStartDateNotBeforeDisbursementDate(LoanDelinquencyAction parsedDelinquencyAction,
LocalDate firstDisbursalDate) {
if (firstDisbursalDate.isAfter(parsedDelinquencyAction.getStartDate())) {
raiseValidationError("loan-delinquency-action-invalid-start-date",
"Start date of pause period must be after first disbursal date", START_DATE);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ class DelinquencyActionParseAndValidatorTest {
public void testParseAndValidationIsOKForPause() throws JsonProcessingException {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
Mockito.when(loan.getDisbursementDate()).thenReturn(localDate("07 September 2022"));

JsonCommand command = delinquencyAction("pause", "09 September 2022", "19 September 2022");

Expand Down Expand Up @@ -96,6 +97,7 @@ public void testParseAndValidationIsOKForResume() throws JsonProcessingException
public void testPauseBothStartAndEndDateIsOverlappingWithAnActivePause() throws JsonProcessingException {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
Mockito.when(loan.getDisbursementDate()).thenReturn(localDate("07 September 2022"));

List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "14 September 2022", "22 September 2022"));
JsonCommand command = delinquencyAction("pause", "09 September 2022", "15 September 2022");
Expand All @@ -111,6 +113,7 @@ public void testPauseBothStartAndEndDateIsOverlappingWithAnActivePause() throws
public void testPauseStartIsOverlappingWithAnActivePause() throws JsonProcessingException {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
Mockito.when(loan.getDisbursementDate()).thenReturn(localDate("11 September 2022"));

List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "14 September 2022", "22 September 2022"));
JsonCommand command = delinquencyAction("pause", "15 September 2022", "23 September 2022");
Expand All @@ -126,7 +129,7 @@ public void testPauseStartIsOverlappingWithAnActivePause() throws JsonProcessing
public void testNewPauseEndIsOverlappingWithExistingPause() throws JsonProcessingException {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);

Mockito.when(loan.getDisbursementDate()).thenReturn(localDate("11 September 2022"));
List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "15 September 2022", "22 September 2022"));
JsonCommand command = delinquencyAction("pause", "13 September 2022", "20 September 2022");
List<LoanDelinquencyActionData> effectiveList = List.of(loanDelinquencyActionData(existing.get(0)));
Expand All @@ -141,6 +144,7 @@ public void testNewPauseEndIsOverlappingWithExistingPause() throws JsonProcessin
public void testNewPauseIsOverlappingWithExistingPauseBecauseSameDates() throws JsonProcessingException {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
Mockito.when(loan.getDisbursementDate()).thenReturn(localDate("11 September 2022"));

List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "15 September 2022", "22 September 2022"));
JsonCommand command = delinquencyAction("pause", "15 September 2022", "22 September 2022");
Expand All @@ -156,6 +160,7 @@ public void testNewPauseIsOverlappingWithExistingPauseBecauseSameDates() throws
public void testNewPauseIsNotOverlappingBecauseThereWasAResume() throws JsonProcessingException {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
Mockito.when(loan.getDisbursementDate()).thenReturn(localDate("11 September 2022"));

JsonCommand command = delinquencyAction("pause", "18 September 2022", "20 September 2022");

Expand Down Expand Up @@ -269,13 +274,15 @@ public void testValidationErrorPausePeriodShouldBeAtLeastOneDay() throws JsonPro
}

@Test
public void testValidationErrorPausePeriodMustBeInFuture() throws JsonProcessingException {
public void testValidationErrorPausePeriodMustNotBeBeforeDisbursement() throws JsonProcessingException {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
Mockito.when(loan.getDisbursementDate()).thenReturn(localDate("11 September 2022"));

JsonCommand command = delinquencyAction("pause", "08 September 2022", "09 September 2022");

assertPlatformValidationException("Start date of pause period must be in the future", "loan-delinquency-action-invalid-start-date",
assertPlatformValidationException("Start date of pause period must be after first disbursal date",
"loan-delinquency-action-invalid-start-date",
() -> underTest.validateAndParseUpdate(command, loan, List.of(), localDate("09 September 2022")));
}

Expand Down Expand Up @@ -308,6 +315,7 @@ public void testStartDateIsMissingForResume() {
public void testNewPausePeriodStartingOnExistingEndDate() throws JsonProcessingException {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
Mockito.when(loan.getDisbursementDate()).thenReturn(localDate("11 September 2022"));

JsonCommand command = delinquencyAction("pause", "18 September 2022", "20 September 2022");

Expand All @@ -324,6 +332,7 @@ public void testNewPausePeriodStartingOnExistingEndDate() throws JsonProcessingE
public void testNewPauseEndingOnExistingStartDate() throws JsonProcessingException {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
Mockito.when(loan.getDisbursementDate()).thenReturn(localDate("11 September 2022"));

JsonCommand command = delinquencyAction("pause", "18 September 2022", "20 September 2022");

Expand All @@ -340,6 +349,7 @@ public void testNewPauseEndingOnExistingStartDate() throws JsonProcessingExcepti
public void testNewPausePeriodStartingOnExistingEffectiveEndDate() throws JsonProcessingException {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
Mockito.when(loan.getDisbursementDate()).thenReturn(localDate("11 September 2022"));

JsonCommand command = delinquencyAction("pause", "18 September 2022", "20 September 2022");

Expand All @@ -355,6 +365,21 @@ public void testNewPausePeriodStartingOnExistingEffectiveEndDate() throws JsonPr
Assertions.assertEquals(localDate("20 September 2022"), parsedDelinquencyAction.getEndDate());
}

@Test
public void testParseAndValidationIsOKForBackdatedPause() throws JsonProcessingException {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
Mockito.when(loan.getDisbursementDate()).thenReturn(localDate("07 September 2022"));

JsonCommand command = delinquencyAction("pause", "08 September 2022", "19 September 2022");

LoanDelinquencyAction parsedDelinquencyAction = underTest.validateAndParseUpdate(command, loan, List.of(),
localDate("09 September 2022"));
Assertions.assertEquals(PAUSE, parsedDelinquencyAction.getAction());
Assertions.assertEquals(localDate("08 September 2022"), parsedDelinquencyAction.getStartDate());
Assertions.assertEquals(localDate("19 September 2022"), parsedDelinquencyAction.getEndDate());
}

@NotNull
private JsonCommand delinquencyAction(@Nullable String action, @Nullable String startDate, @Nullable String endDate)
throws JsonProcessingException {
Expand Down
Loading

0 comments on commit 6526e10

Please sign in to comment.